diff --git a/CHANGELOG.md b/CHANGELOG.md index 7737041..7c146f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.1.3] - 04.02.2018 +* Inform client if the functionality was not activated on the server +* Client side unit tests + ## [0.1.2] - 01.02.2018 * Remove 'lodash' dependency, replace usages using ES6 * Remove 'crypto-js' dependency, use 'crypto' instead @@ -10,4 +14,4 @@ * Add server side tests ## [0.1.0] - 19.01.2018 -* Initial release \ No newline at end of file +* Initial release diff --git a/client/index.js b/client/index.js index 1f5dbdc..bf5499d 100644 --- a/client/index.js +++ b/client/index.js @@ -1,34 +1,38 @@ -import { Accounts } from 'meteor/accounts-base'; import overrideAccountsLogin from './overrideLogin'; +overrideAccountsLogin(); const RememberMe = {}; const updateRememberMe = 'tprzytula:rememberMe-update'; RememberMe.loginWithPassword = (user, password, callback = () => {}, rememberMe = true) => { - let flag = rememberMe; - let callbackMethod = () => {}; + const flag = (typeof callback === 'boolean') + ? callback + : rememberMe; - if (typeof callback === 'boolean') { - flag = callback; - } else { - callbackMethod = callback; - } + const callbackMethod = (typeof callback === 'function') + ? callback + : () => {}; Meteor.loginWithPassword(user, password, (error) => { if (!error) { - Meteor.call(updateRememberMe, flag); + Meteor.call(updateRememberMe, flag, (error) => { + if (error && error.error === 404) { + console.warn( + 'Dependency meteor/tprzytula:remember-me is not active!\n', + '\nTo activate it make sure to run "RememberMe.activate()" on the server.' + + 'It is required to be able to access the functionality on the client.' + ) + } else if (error) { + console.error( + 'meteor/tprzytula:remember-me' + + '\nCould not update remember me setting.' + + '\nError:', error + ); + } + }); } callbackMethod(error); }); }; -let loginOverridden = false; -Accounts.onLogin(() => { - /* Override meteor accounts callLoginMethod to store information that user logged before */ - if (!loginOverridden) { - overrideAccountsLogin(); - loginOverridden = true; - } -}); - export default RememberMe; diff --git a/client/overrideLogin.js b/client/overrideLogin.js index f52ed22..bdf082f 100644 --- a/client/overrideLogin.js +++ b/client/overrideLogin.js @@ -1,3 +1,5 @@ +import { Accounts } from 'meteor/accounts-base'; + /** * This function is used to override Account's function called callLoginMethod. * We are using our custom implementation of remember me functionality where user @@ -19,7 +21,7 @@ * Launching the app again means that it will became default again without our additions. * Then the next successful login will override it again. */ -export default () => { +const overrideLoginMethod = () => { const accountsCallLoginMethod = Accounts.callLoginMethod.bind(Accounts); Accounts.callLoginMethod = function (options = {}) { const preparedOptions = options; @@ -33,3 +35,14 @@ export default () => { accountsCallLoginMethod(preparedOptions); }; }; + +export default () => { + let loginOverridden = false; + Accounts.onLogin(() => { + /* Override meteor accounts callLoginMethod to store information that user logged before */ + if (!loginOverridden) { + overrideLoginMethod(); + loginOverridden = true; + } + }); +}; diff --git a/package.js b/package.js index 9985dab..6a0efc4 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'tprzytula:remember-me', - version: '0.1.2', + version: '0.1.3', summary: 'Extension for Meteor account-base package with the implementation of rememberMe', git: 'https://github.com/tprzytulacc/Meteor-RememberMe', documentation: 'README.md' @@ -23,5 +23,11 @@ Package.onTest((api) => { api.use('meteortesting:mocha'); api.use('tprzytula:remember-me'); api.use('practicalmeteor:chai'); + api.use('practicalmeteor:sinon'); + api.mainModule('test/client/index.js', 'client'); api.mainModule('test/server/index.js', 'server'); -}); + Npm.depends({ + sinon: '4.2.2' + }); + +}); \ No newline at end of file diff --git a/package.json b/package.json index 2758f28..9d45d36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rememberMe", - "version": "0.1.2", + "version": "0.1.3", "description": "Extension for Meteor account-base package with the implementation of rememberMe", "license": "MIT", "author": "Tomasz Przytuła ", @@ -16,6 +16,7 @@ ], "homepage": "https://github.com/tprzytulacc/Meteor-Remember-Me", "scripts": { - "test": "meteor test-packages ./ --driver-package meteortesting:mocha" + "test": "meteor test-packages ./ --once --driver-package meteortesting:mocha", + "test-watch": "TEST_WATCH=1 meteor test-packages ./ --full-app --driver-package meteortesting:mocha" } -} +} \ No newline at end of file diff --git a/test/client/index.js b/test/client/index.js new file mode 100644 index 0000000..b8df722 --- /dev/null +++ b/test/client/index.js @@ -0,0 +1,10 @@ +const method = require('./tests/method'); +const rememberMe = require('./tests/rememberMe'); + +/** + * Client-side test cases. + */ +describe('client', () => { + method(); + rememberMe(); +}); \ No newline at end of file diff --git a/test/client/tests/method.js b/test/client/tests/method.js new file mode 100644 index 0000000..8a11e93 --- /dev/null +++ b/test/client/tests/method.js @@ -0,0 +1,120 @@ +const RememberMe = require('meteor/tprzytula:remember-me').default; +const rememberMeMethod = 'tprzytula:rememberMe-update'; +const sinon = require('sinon'); + +module.exports = () => { + /** + * RememberMe.loginWithPassword is a wrapper for the Meteor's + * loginWithPassword method. It should match specific behaviour. + */ + describe('Invoking "RememberMe.loginWithPassword" method', () => { + /** + * Each time the wrapper is called it + * should also internally call the Meteor's login method. + */ + it('should call Meteor.loginWithPassword', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + + RememberMe.loginWithPassword('username', 'password'); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + + RememberMe.loginWithPassword('username_two', 'password_two'); + expect(loginWithPassword).to.have.been.calledTwice; + expect(loginWithPassword).to.have.been.calledWith( + 'username_two', + 'password_two' + ); + + loginWithPassword.restore(); + }); + + /** + * Updating the Remember Me status is only relevant if the login + * performed successfully. Otherwise there is no reason to send + * this request to the server. + */ + it('should not call updateRememberMe method if login failed', () => { + const loginWithPassword = sinon.stub(Meteor, 'loginWithPassword'); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = 'Invalid user'; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password'); + expect(loginWithPassword).to.have.been.calledOnce; + expect(call).to.have.callCount(0); + + loginWithPassword.restore(); + call.restore(); + }); + + /** + * If the login performed successfully then method for updating + * the state of remember me should be invoked. This way server + * will be informed about requested change for this setting. + */ + it('should call updateRememberMe method if logged in successfully', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password'); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWith(rememberMeMethod); + + loginWithPassword.restore(); + call.restore(); + }); + + /** + * Meteor.loginWithPassword takes a callback as a parameter. + * The callback provided to the RememberMe.loginWithPassword + * should be invoked after received response from the Meteor's + * internal login method. It should also sent the error as + * a parameter (if encountered). + */ + it('should correctly pass callback to "Meteor.loginWithPassword"', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + const error = 'User does not exist'; + loginWithPassword.callsFake((user, password, callback) => { + callback(error); + }); + const obj = {}; + obj.callback = (error) => error; + const callbackSpy = sinon.spy(obj, 'callback'); + RememberMe.loginWithPassword('username', 'password', obj.callback); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + + expect(callbackSpy).to.have.been.calledOnce; + expect(callbackSpy).to.have.been.calledWith(error); + + loginWithPassword.restore(); + call.restore(); + }); + }); +}; \ No newline at end of file diff --git a/test/client/tests/rememberMe.js b/test/client/tests/rememberMe.js new file mode 100644 index 0000000..5f77011 --- /dev/null +++ b/test/client/tests/rememberMe.js @@ -0,0 +1,170 @@ +const RememberMe = require('meteor/tprzytula:remember-me').default; +const rememberMeMethod = 'tprzytula:rememberMe-update'; +const sinon = require('sinon'); + +module.exports = () => { + /** + * Remember me setting should be correctly set for each case. + */ + describe('Remember me flag', () => { + + /** + * There is no requirement for passing remember me parameter. + * To match default behaviour of Meteor it's true by default. + */ + describe('not provided as a parameter', () => { + + it('should set remember me to true by default (without callback)', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password'); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWith(rememberMeMethod, true); + + loginWithPassword.restore(); + call.restore(); + }); + + it('should set remember me to true by default (with callback)', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password', () => {}); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWith(rememberMeMethod, true); + + loginWithPassword.restore(); + call.restore(); + }); + }); + + /** + * Remember me flag can be provided as a third parameter + * in case where user does not need to provide a callback. + */ + describe('provided as a third parameter', () => { + + it('should set remember me to true if equals "true"', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password', true); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWithMatch(rememberMeMethod, true); + + loginWithPassword.restore(); + call.restore(); + }); + + it('should set remember me to false if equals "false"', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password', false); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWithMatch(rememberMeMethod, false); + + loginWithPassword.restore(); + call.restore(); + }); + }); + + /** + * Remember me flag can be provided as a fourth parameter + * in case where wants to provide callback as a third one. + */ + describe('provided as a fourth parameter', () => { + + it('should set remember me to true if equals "true"', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password', () => {}, true); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWithMatch(rememberMeMethod, true); + + loginWithPassword.restore(); + call.restore(); + }); + + it('should set remember me to false if equals "false"', () => { + const loginWithPassword = sinon.stub( + Meteor, + 'loginWithPassword' + ); + const call = sinon.stub(Meteor, 'call'); + loginWithPassword.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + RememberMe.loginWithPassword('username', 'password', () => {}, false); + expect(loginWithPassword).to.have.been.calledOnce; + expect(loginWithPassword).to.have.been.calledWith( + 'username', + 'password' + ); + expect(call).to.have.been.calledOnce; + expect(call).to.have.been.calledWithMatch(rememberMeMethod, false); + + loginWithPassword.restore(); + call.restore(); + }); + }); + }); +}; \ No newline at end of file