diff --git a/.slugignore b/.slugignore
index 7c0e001e12..9bdd15ef66 100644
--- a/.slugignore
+++ b/.slugignore
@@ -1,3 +1,7 @@
# Files not included in deployments to Heroku, to save on file size.
/habitica-images
+/test
+/migrations
+/scripts
+/database_reports
diff --git a/package-lock.json b/package-lock.json
index da6bdae4b2..0fb708f499 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,24 +26,24 @@
"integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q=="
},
"@babel/core": {
- "version": "7.17.8",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz",
- "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz",
+ "integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.16.7",
- "@babel/generator": "^7.17.7",
+ "@babel/generator": "^7.17.9",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/helper-module-transforms": "^7.17.7",
- "@babel/helpers": "^7.17.8",
- "@babel/parser": "^7.17.8",
+ "@babel/helpers": "^7.17.9",
+ "@babel/parser": "^7.17.9",
"@babel/template": "^7.16.7",
- "@babel/traverse": "^7.17.3",
+ "@babel/traverse": "^7.17.9",
"@babel/types": "^7.17.0",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
- "json5": "^2.1.2",
+ "json5": "^2.2.1",
"semver": "^6.3.0"
},
"dependencies": {
@@ -61,9 +61,9 @@
"integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ=="
},
"@babel/generator": {
- "version": "7.17.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
- "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
+ "integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
"requires": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
@@ -81,6 +81,15 @@
"semver": "^6.3.0"
}
},
+ "@babel/helper-function-name": {
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz",
+ "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==",
+ "requires": {
+ "@babel/template": "^7.16.7",
+ "@babel/types": "^7.17.0"
+ }
+ },
"@babel/helper-module-transforms": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz",
@@ -110,9 +119,9 @@
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
},
"@babel/highlight": {
- "version": "7.16.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
- "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz",
+ "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"chalk": "^2.0.0",
@@ -120,22 +129,22 @@
}
},
"@babel/parser": {
- "version": "7.17.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
- "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
+ "integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
},
"@babel/traverse": {
- "version": "7.17.3",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
- "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
+ "integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
"requires": {
"@babel/code-frame": "^7.16.7",
- "@babel/generator": "^7.17.3",
+ "@babel/generator": "^7.17.9",
"@babel/helper-environment-visitor": "^7.16.7",
- "@babel/helper-function-name": "^7.16.7",
+ "@babel/helper-function-name": "^7.17.9",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
- "@babel/parser": "^7.17.3",
+ "@babel/parser": "^7.17.9",
"@babel/types": "^7.17.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
@@ -160,6 +169,11 @@
"supports-color": "^5.3.0"
}
},
+ "json5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -418,12 +432,12 @@
}
},
"@babel/helpers": {
- "version": "7.17.8",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz",
- "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz",
+ "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==",
"requires": {
"@babel/template": "^7.16.7",
- "@babel/traverse": "^7.17.3",
+ "@babel/traverse": "^7.17.9",
"@babel/types": "^7.17.0"
},
"dependencies": {
@@ -436,24 +450,33 @@
}
},
"@babel/generator": {
- "version": "7.17.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
- "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
+ "integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
"requires": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
+ "@babel/helper-function-name": {
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz",
+ "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==",
+ "requires": {
+ "@babel/template": "^7.16.7",
+ "@babel/types": "^7.17.0"
+ }
+ },
"@babel/helper-validator-identifier": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
},
"@babel/highlight": {
- "version": "7.16.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
- "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz",
+ "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"chalk": "^2.0.0",
@@ -461,22 +484,22 @@
}
},
"@babel/parser": {
- "version": "7.17.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
- "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
+ "integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
},
"@babel/traverse": {
- "version": "7.17.3",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
- "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
+ "version": "7.17.9",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
+ "integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
"requires": {
"@babel/code-frame": "^7.16.7",
- "@babel/generator": "^7.17.3",
+ "@babel/generator": "^7.17.9",
"@babel/helper-environment-visitor": "^7.16.7",
- "@babel/helper-function-name": "^7.16.7",
+ "@babel/helper-function-name": "^7.17.9",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
- "@babel/parser": "^7.17.3",
+ "@babel/parser": "^7.17.9",
"@babel/types": "^7.17.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
@@ -10794,9 +10817,9 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
},
"nconf": {
- "version": "0.11.3",
- "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.11.3.tgz",
- "integrity": "sha512-iYsAuDS9pzjVMGIzJrGE0Vk3Eh8r/suJanRAnWGBd29rVS2XtSgzcAo5l6asV3e4hH2idVONHirg1efoBOslBg==",
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.11.4.tgz",
+ "integrity": "sha512-YaDR846q11JnG1vTrhJ0QIlhiGY6+W1bgWtReG9SS3vkTl3AoNwFvUItdhG6/ZjGCfWpUVuRTNEBTDAQ3nWhGw==",
"requires": {
"async": "^1.4.0",
"ini": "^2.0.0",
@@ -10805,9 +10828,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
@@ -10856,11 +10879,11 @@
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
},
"strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
- "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"requires": {
- "ansi-regex": "^5.0.0"
+ "ansi-regex": "^5.0.1"
}
},
"wrap-ansi": {
@@ -13779,12 +13802,12 @@
}
},
"stripe": {
- "version": "8.212.0",
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.212.0.tgz",
- "integrity": "sha512-xQ2uPMRAmRyOiMZktw3hY8jZ8LFR9lEQRPEaQ5WcDcn51kMyn46GeikOikxiFTHEN8PeKRdwtpz4yNArAvu/Kg==",
+ "version": "8.216.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.216.0.tgz",
+ "integrity": "sha512-LY8cNGizEnklIa4T82l6mZW0HS4cfzo1hNuhT+ZR9PBkmYcSUbg3ilUBVF0FCd4RP+NA44VEVfoSTTZ1Gg5+rQ==",
"requires": {
"@types/node": ">=8.1.0",
- "qs": "^6.6.0"
+ "qs": "^6.10.3"
},
"dependencies": {
"qs": {
@@ -15490,9 +15513,9 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw=="
},
"winston": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/winston/-/winston-3.6.0.tgz",
- "integrity": "sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w==",
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-3.7.2.tgz",
+ "integrity": "sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==",
"requires": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
diff --git a/package.json b/package.json
index 627eb99a00..6c3ae30739 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "4.228.1",
"main": "./website/server/index.js",
"dependencies": {
- "@babel/core": "^7.17.8",
+ "@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/register": "^7.17.7",
"@google-cloud/trace-agent": "^5.1.6",
@@ -51,7 +51,7 @@
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
- "nconf": "^0.11.3",
+ "nconf": "^0.11.4",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
@@ -67,14 +67,14 @@
"remove-markdown": "^0.3.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.0",
- "stripe": "^8.212.0",
+ "stripe": "^8.216.0",
"superagent": "^7.1.2",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vinyl-buffer": "^1.0.1",
- "winston": "^3.6.0",
+ "winston": "^3.7.2",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
},
diff --git a/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js b/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js
index 975fccfa3f..b1c97f6ca4 100644
--- a/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js
+++ b/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js
@@ -1,3 +1,4 @@
+import { v4 as generateUUID } from 'uuid';
import {
generateUser,
requester,
@@ -9,15 +10,18 @@ describe('GET /user/auth/apple', () => {
let api;
let user;
const appleEndpoint = '/user/auth/apple';
-
- before(async () => {
- const expectedResult = { id: 'appleId', name: 'an apple user' };
- sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
- });
+ let randomAppleId = '123456';
beforeEach(async () => {
api = requester();
user = await generateUser();
+ randomAppleId = generateUUID();
+ const expectedResult = { id: randomAppleId, name: 'an apple user' };
+ sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
+ });
+
+ afterEach(async () => {
+ appleAuth.appleProfile.restore();
});
it('registers a new user', async () => {
@@ -26,7 +30,7 @@ describe('GET /user/auth/apple', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
- await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
+ await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal(randomAppleId);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
});
diff --git a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js
index eff5525603..c4ffe57efb 100644
--- a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js
+++ b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js
@@ -1,4 +1,5 @@
import passport from 'passport';
+import { v4 as generateUUID } from 'uuid';
import {
generateUser,
requester,
@@ -10,14 +11,15 @@ describe('POST /user/auth/social', () => {
let api;
let user;
const endpoint = '/user/auth/social';
- const randomAccessToken = '123456';
- const facebookId = 'facebookId';
- const googleId = 'googleId';
+ let randomAccessToken = '123456';
+ let randomFacebookId = 'facebookId';
+ let randomGoogleId = 'googleId';
let network = 'NoNetwork';
beforeEach(async () => {
api = requester();
user = await generateUser();
+ randomAccessToken = generateUUID();
});
it('fails if network is not supported', async () => {
@@ -32,12 +34,23 @@ describe('POST /user/auth/social', () => {
});
describe('facebook', () => {
- before(async () => {
- const expectedResult = { id: facebookId, displayName: 'a facebook user' };
+ beforeEach(async () => {
+ randomFacebookId = generateUUID();
+ const expectedResult = {
+ id: randomFacebookId,
+ displayName: 'a facebook user',
+ emails: [
+ { value: `${user.auth.local.username}+facebook@example.com` },
+ ],
+ };
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
+ afterEach(async () => {
+ passport._strategies.facebook.userProfile.restore();
+ });
+
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -51,7 +64,8 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
- await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
+ await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`);
+ await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
});
it('logs an existing user in', async () => {
@@ -68,6 +82,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
+ expect(registerResponse.newUser).to.be.true;
+ });
+
+ it('logs an existing user in if they have local auth with matching email', async () => {
+ passport._strategies.facebook.userProfile.restore();
+ const expectedResult = {
+ id: randomFacebookId,
+ displayName: 'a facebook user',
+ emails: [
+ { value: user.auth.local.email },
+ ],
+ };
+ sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
+
+ const response = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+
+ expect(response.apiToken).to.eql(user.apiToken);
+ expect(response.id).to.eql(user._id);
+ expect(response.newUser).to.be.false;
+ });
+
+ it('logs an existing user into their social account if they have local auth with matching email', async () => {
+ const registerResponse = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+ expect(registerResponse.newUser).to.be.true;
+ // This is important for existing accounts before the new social handling
+ passport._strategies.facebook.userProfile.restore();
+ const expectedResult = {
+ id: randomFacebookId,
+ displayName: 'a facebook user',
+ emails: [
+ { value: user.auth.local.email },
+ ],
+ };
+ sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
+
+ const response = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+
+ expect(response.apiToken).to.eql(registerResponse.apiToken);
+ expect(response.id).to.eql(registerResponse.id);
+ expect(response.apiToken).not.to.eql(user.apiToken);
+ expect(response.id).not.to.eql(user._id);
+ expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
@@ -76,11 +141,28 @@ describe('POST /user/auth/social', () => {
network,
});
- expect(response.apiToken).to.exist;
- expect(response.id).to.exist;
+ expect(response.apiToken).to.eql(user.apiToken);
+ expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
+ it('does not log into other account if social auth already exists', async () => {
+ const registerResponse = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+ expect(registerResponse.newUser).to.be.true;
+
+ await expect(user.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ })).to.eventually.be.rejected.and.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('socialAlreadyExists'),
+ });
+ });
+
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -92,12 +174,23 @@ describe('POST /user/auth/social', () => {
});
describe('google', () => {
- before(async () => {
- const expectedResult = { id: googleId, displayName: 'a google user' };
+ beforeEach(async () => {
+ randomGoogleId = generateUUID();
+ const expectedResult = {
+ id: randomGoogleId,
+ displayName: 'a google user',
+ emails: [
+ { value: `${user.auth.local.username}+google@example.com` },
+ ],
+ };
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google';
});
+ afterEach(async () => {
+ passport._strategies.google.userProfile.restore();
+ });
+
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -107,7 +200,8 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
- await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
+ await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(randomGoogleId);
+ await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+google@example.com`);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
@@ -125,6 +219,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
+ expect(registerResponse.newUser).to.be.true;
+ });
+
+ it('logs an existing user in if they have local auth with matching email', async () => {
+ passport._strategies.google.userProfile.restore();
+ const expectedResult = {
+ id: randomGoogleId,
+ displayName: 'a google user',
+ emails: [
+ { value: user.auth.local.email },
+ ],
+ };
+ sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
+
+ const response = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+
+ expect(response.apiToken).to.eql(user.apiToken);
+ expect(response.id).to.eql(user._id);
+ expect(response.newUser).to.be.false;
+ });
+
+ it('logs an existing user into their social account if they have local auth with matching email', async () => {
+ const registerResponse = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+ expect(registerResponse.newUser).to.be.true;
+ // This is important for existing accounts before the new social handling
+ passport._strategies.google.userProfile.restore();
+ const expectedResult = {
+ id: randomGoogleId,
+ displayName: 'a google user',
+ emails: [
+ { value: user.auth.local.email },
+ ],
+ };
+ sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
+
+ const response = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+
+ expect(response.apiToken).to.eql(registerResponse.apiToken);
+ expect(response.id).to.eql(registerResponse.id);
+ expect(response.apiToken).not.to.eql(user.apiToken);
+ expect(response.id).not.to.eql(user._id);
+ expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
@@ -133,11 +278,28 @@ describe('POST /user/auth/social', () => {
network,
});
- expect(response.apiToken).to.exist;
- expect(response.id).to.exist;
+ expect(response.apiToken).to.eql(user.apiToken);
+ expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
+ it('does not log into other account if social auth already exists', async () => {
+ const registerResponse = await api.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ });
+ expect(registerResponse.newUser).to.be.true;
+
+ await expect(user.post(endpoint, {
+ authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+ network,
+ })).to.eventually.be.rejected.and.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('socialAlreadyExists'),
+ });
+ });
+
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
diff --git a/test/common/libs/cron.test.js b/test/common/libs/cron.test.js
index 0894a6afa3..bd84de1e80 100644
--- a/test/common/libs/cron.test.js
+++ b/test/common/libs/cron.test.js
@@ -1,6 +1,6 @@
import moment from 'moment';
-import { startOfDay, daysSince } from '../../../website/common/script/cron';
+import { startOfDay, daysSince, getPlanContext } from '../../../website/common/script/cron';
function localMoment (timeString, utcOffset) {
return moment(timeString).utcOffset(utcOffset, true);
@@ -181,4 +181,63 @@ describe('cron utility functions', () => {
expect(result).to.equal(0);
});
});
+
+ describe('getPlanContext', () => {
+ const now = new Date(2022, 5, 1);
+
+ function baseUserData (count, offset, planId) {
+ return {
+ purchased: {
+ plan: {
+ consecutive: {
+ count,
+ offset,
+ gemCapExtra: 25,
+ trinkets: 19,
+ },
+ quantity: 1,
+ extraMonths: 0,
+ gemsBought: 0,
+ owner: '116b4133-8fb7-43f2-b0de-706621a8c9d8',
+ nextBillingDate: null,
+ nextPaymentProcessing: null,
+ planId,
+ customerId: 'group-plan',
+ dateUpdated: '2022-05-10T03:00:00.144+01:00',
+ paymentMethod: 'Group Plan',
+ dateTerminated: null,
+ lastBillingDate: null,
+ dateCreated: '2017-02-10T19:00:00.355+01:00',
+ },
+ },
+ };
+ }
+
+ it('offset 0, next date in 3 months', () => {
+ const user = baseUserData(60, 0, 'group_plan_auto');
+
+ const planContext = getPlanContext(user, now);
+
+ expect(planContext.nextHourglassDate)
+ .to.be.sameMoment('2022-08-10T02:00:00.144Z');
+ });
+
+ it('offset 1, next date in 1 months', () => {
+ const user = baseUserData(60, 1, 'group_plan_auto');
+
+ const planContext = getPlanContext(user, now);
+
+ expect(planContext.nextHourglassDate)
+ .to.be.sameMoment('2022-06-10T02:00:00.144Z');
+ });
+
+ it('offset 2, next date in 2 months - with any plan', () => {
+ const user = baseUserData(60, 2, 'basic_3mo');
+
+ const planContext = getPlanContext(user, now);
+
+ expect(planContext.nextHourglassDate)
+ .to.be.sameMoment('2022-07-10T02:00:00.144Z');
+ });
+ });
});
diff --git a/website/client/config/storybook/mock.data.js b/website/client/config/storybook/mock.data.js
index 0d9a57a8b2..5143c23348 100644
--- a/website/client/config/storybook/mock.data.js
+++ b/website/client/config/storybook/mock.data.js
@@ -1,4 +1,5 @@
import { v4 as generateUUID } from 'uuid';
+import getters from '@/store/getters';
export const userStyles = {
contributor: {
@@ -82,3 +83,25 @@ export const userStyles = {
classSelected: true,
},
};
+
+
+export function mockStore ({
+ userData,
+ ...state
+}) {
+ return {
+ getters,
+ dispatch: () => {
+ },
+ watch: () => {
+ },
+ state: {
+ user: {
+ data: {
+ ...userData,
+ },
+ },
+ ...state,
+ },
+ };
+}
diff --git a/website/client/src/components/payments/buttons/list.stories.js b/website/client/src/components/payments/buttons/list.stories.js
index 780f365c01..2142cb9bef 100644
--- a/website/client/src/components/payments/buttons/list.stories.js
+++ b/website/client/src/components/payments/buttons/list.stories.js
@@ -7,7 +7,7 @@ import { setup as setupPayments } from '@/libs/payments';
setupPayments();
-storiesOf('Payments Buttons', module)
+storiesOf('Subscriptions/Payments Buttons', module)
.add('simple', () => ({
components: { PaymentsButtonsList },
template: `
diff --git a/website/client/src/components/settings/dayStartAdjustment.vue b/website/client/src/components/settings/dayStartAdjustment.vue
new file mode 100644
index 0000000000..54d312740c
--- /dev/null
+++ b/website/client/src/components/settings/dayStartAdjustment.vue
@@ -0,0 +1,132 @@
+
+ {{ $t('dayStartAdjustment') }}
+ {{ $t('adjustment') }}
+