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 @@ + + + + + diff --git a/website/client/src/components/settings/site.vue b/website/client/src/components/settings/site.vue index 9ee523384a..389ee7f1bf 100644 --- a/website/client/src/components/settings/site.vue +++ b/website/client/src/components/settings/site.vue @@ -213,49 +213,7 @@ {{ $t('enableClass') }}
-
-
{{ $t('customDayStart') }}
-
- {{ $t('customDayStartInfo1') }} -
-
-
-
- -
-
- -
-
-
-
-
-
{{ $t('timezone') }}
-
-
-
-

-

-
-
-
+
@@ -268,7 +226,7 @@ :key="network.key" >
-
+
{{ $t('changeEmail') }}
@import '~@/assets/scss/colors.scss'; - input { color: $gray-50; } - .usersettings h5 { margin-top: 1em; } - .iconalert > div > span { line-height: 25px; } - .iconalert > div:after { clear: both; content: ''; display: table; } - .input-error { color: $red-50; font-size: 90%; @@ -568,16 +523,15 @@