diff --git a/.eslintrc b/.eslintrc index dbd8924..061fb58 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,9 @@ "globals": { "Meteor": true, "it": true, - "describe": true + "describe": true, + "beforeEach": true, + "afterEach": true }, "rules": { "comma-dangle": [ @@ -34,6 +36,8 @@ "never" ], "no-underscore-dangle": 0, - "no-console": 0 + "no-console": 0, + "one-var-declaration-per-line": 0, + "one-var": 0 } } diff --git a/.gitignore b/.gitignore index 6f4987f..6d04298 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea +.coverage node_modules/ npm-debug.log \ No newline at end of file diff --git a/.meteor/packages b/.meteor/packages index d13d405..8b6296a 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -7,16 +7,17 @@ meteor-base@1.3.0 # Packages every Meteor app needs to have mobile-experience@1.0.5 # Packages for a great mobile UX mongo@1.4.2 # The database Meteor supports right now -blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views -reactive-var@1.0.11 # Reactive variable for tracker +blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views +reactive-var@1.0.11 # Reactive variable for tracker tracker@1.1.3 # Meteor's client-side reactive programming library standard-minifier-css@1.4.0 # CSS minifier run for production mode standard-minifier-js@2.3.1 # JS minifier run for production mode es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers -ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code shell-server@0.3.1 # Server-side component of the `meteor shell` command tprzytula:remember-me accounts-base meteortesting:mocha +lmieulet:meteor-coverage diff --git a/.meteorignore b/.meteorignore index 9de84ce..ee6f167 100644 --- a/.meteorignore +++ b/.meteorignore @@ -1 +1,2 @@ -package.js \ No newline at end of file +package.js +.coverage \ No newline at end of file diff --git a/.npmignore b/.npmignore index c3d07e0..9a90d12 100644 --- a/.npmignore +++ b/.npmignore @@ -17,3 +17,4 @@ appveyor.yml .travis.yml node_modules npm-debug.log +.coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 354135a..5ed8d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,6 @@ addons: before_install: - "curl -L https://raw.githubusercontent.com/arunoda/travis-ci-meteor-packages/dca8e51fafd60d9e5a8285b07ca34a63f22a5ed4/configure.sh | /bin/sh" before_script: - - "export PATH=$HOME/.meteor:$PATH" \ No newline at end of file + - "export PATH=$HOME/.meteor:$PATH" +after_success: + - npm run coveralls \ No newline at end of file diff --git a/README.md b/README.md index 7874038..bfdf5f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Meteor - Remember Me +# Meteor - Remember Me +[![Build Status](https://travis-ci.org/tprzytula/Meteor-Remember-Me.svg?branch=master)](https://travis-ci.org/tprzytula/Meteor-Remember-Me) [![Coverage Status](https://coveralls.io/repos/github/tprzytula/Meteor-Remember-Me/badge.svg)](https://coveralls.io/github/tprzytula/Meteor-Remember-Me) ### Integrated remember me functionality support for Meteor diff --git a/client/index.js b/client/index.js index 306ecff..5a1e2b7 100644 --- a/client/index.js +++ b/client/index.js @@ -135,6 +135,7 @@ class RememberMe { } /* eslint-disable */ + /* istanbul ignore next */ /* The method is based on the original one in Accounts: https://github.com/meteor/meteor/blob/46257bad264bf089e35e0fe35494b51fe5849c7b/packages/accounts-password/password_client.js#L33 diff --git a/package-lock.json b/package-lock.json index e89eaa6..407ce66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rememberMe", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -833,6 +833,27 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "coveralls": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.1.tgz", + "integrity": "sha512-FAzXwiDOYLGDWH+zgoIA+8GbWv50hlx+kpEJyvzLKOdnIBv9uWoVl4DhqGgyUHpiRjAlF8KYZSipWXYtllWH6Q==", + "dev": true, + "requires": { + "js-yaml": "3.11.0", + "lcov-parse": "0.0.10", + "log-driver": "1.2.7", + "minimist": "1.2.0", + "request": "2.85.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "cross-env": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.1.4.tgz", @@ -1990,6 +2011,12 @@ "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", "dev": true }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -2054,6 +2081,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "lolex": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz", diff --git a/package.json b/package.json index cf86f48..4ccdfd1 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,16 @@ "eslint": "eslint ./client ./server ./test || true", "eslint-check": "eslint ./client ./server ./test", "eslint-fix": "eslint ./client ./server ./test --fix || true", - "test": "cross-env TEST_BROWSER_DRIVER=chrome meteor test --once --driver-package meteortesting:mocha", - "test-watch": "cross-env WATCH=1 TEST_BROWSER_DRIVER=chrome meteor test --driver-package meteortesting:mocha" + "test": "cross-env TEST_BROWSER_DRIVER=chrome COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_OUT_HTML=1 COVERAGE_OUT_TEXT_SUMMARY=1 COVERAGE_APP_FOLDER=$PWD/ meteor test --once --driver-package meteortesting:mocha", + "test-watch": "cross-env WATCH=1 TEST_BROWSER_DRIVER=chrome meteor test --driver-package meteortesting:mocha", + "coveralls": "cat .coverage/lcov.info | coveralls" }, "devDependencies": { "@babel/runtime": "^7.0.0-beta.44", "babel-eslint": "^8.2.2", "bcrypt": "^2.0.0", "chromedriver": "^2.37.0", + "coveralls": "^3.0.1", "cross-env": "^5.1.4", "eslint": "4.18.2", "eslint-config-airbnb-base": "^12.1.0", diff --git a/test/client/helpers.spec.js b/test/client/helpers.spec.js new file mode 100644 index 0000000..1e8cffa --- /dev/null +++ b/test/client/helpers.spec.js @@ -0,0 +1,121 @@ +const { exportFlagFromParams, exportCallbackFromParams } = require('./../../client/helpers'); +const chai = require('ultimate-chai'); + +const { expect } = chai; + +describe('Given exportFlagFromParams', () => { + const sampleFlag = false; + const sampleMethod = () => {}; + let result; + + describe('When rememberMe flag is a first param', () => { + beforeEach(() => { + result = exportFlagFromParams([sampleFlag]); + }); + + it('should return the flag', () => { + expect(result).to.be.equal(sampleFlag); + }); + }); + + describe('When rememberMe flag is a second param', () => { + beforeEach(() => { + result = exportFlagFromParams([sampleMethod, sampleFlag]); + }); + + it('should return the flag', () => { + expect(result).to.be.equal(sampleFlag); + }); + }); + + describe('When parameters are only containing a method', () => { + beforeEach(() => { + result = exportFlagFromParams([sampleMethod]); + }); + + it('should return true as default', () => { + expect(result).to.be.equal(true); + }); + }); + + describe('When the parameters are empty', () => { + beforeEach(() => { + result = exportFlagFromParams([]); + }); + + it('should return true as default', () => { + expect(result).to.be.equal(true); + }); + }); + + describe('When nothing is passed to the method', () => { + beforeEach(() => { + result = exportFlagFromParams(); + }); + + it('should return true as default', () => { + expect(result).to.be.equal(true); + }); + }); +}); + +describe('Given exportCallbackFromParams', () => { + const sampleMethod = () => {}; + let result; + + describe('When function is a first param', () => { + beforeEach(() => { + result = exportCallbackFromParams([sampleMethod, 1, 'abc']); + }); + + it('should return the same method', () => { + expect(result).to.be.equal(sampleMethod); + }); + }); + + describe('When function is a second param', () => { + beforeEach(() => { + result = exportCallbackFromParams([1, sampleMethod, 'abc']); + }); + + it('should return a function', () => { + expect(typeof result).to.be.equal('function'); + }); + + describe('And the returned function', () => { + it('should not be the same as the one in params', () => { + expect(result).to.not.be.equal(sampleMethod); + }); + }); + }); + + describe('When function is not in parameters', () => { + beforeEach(() => { + result = exportCallbackFromParams([1, 'abc']); + }); + + it('should return a function', () => { + expect(typeof result).to.be.equal('function'); + }); + }); + + describe('When the parameters are empty', () => { + beforeEach(() => { + result = exportCallbackFromParams([]); + }); + + it('should return a function', () => { + expect(typeof result).to.be.equal('function'); + }); + }); + + describe('When nothing is passed to the method', () => { + beforeEach(() => { + result = exportCallbackFromParams(); + }); + + it('should return a function', () => { + expect(typeof result).to.be.equal('function'); + }); + }); +}); diff --git a/test/client/index.spec.js b/test/client/index.spec.js index f89322a..b6912bb 100644 --- a/test/client/index.spec.js +++ b/test/client/index.spec.js @@ -1,14 +1,354 @@ -const method = require('./tests/method'); -const rememberMe = require('./tests/rememberMe'); -const customAccountsClient = require('./tests/customAccountsClient'); -const overrideLogin = require('./tests/overrideLogin'); - -/** - * Client-side test cases. - */ -describe('client', () => { - method(); - rememberMe(); - customAccountsClient(); - overrideLogin(); +const RememberMe = require('./../../client/index').RememberMeClass; +const { AccountsClient } = require('meteor/accounts-base'); +const chai = require('ultimate-chai'); +const sinon = require('sinon'); + +const rememberMeMethod = 'tprzytula:rememberMe-update'; +const sampleUsername = 'username'; +const samplePassword = 'password'; +const sampleCallback = () => {}; +const { expect } = chai; + +describe('Given updateRememberMe method', () => { + let callStub, loginWithPasswordStub, rememberMe; + + beforeEach(() => { + rememberMe = new RememberMe(); + loginWithPasswordStub = sinon.stub(Meteor, 'loginWithPassword'); + callStub = sinon.stub(Meteor, 'call'); + }); + + afterEach(() => { + loginWithPasswordStub.restore(); + callStub.restore(); + }); + + describe('When login was unsuccessful', () => { + const notExistingUsername = 'notExistingUser'; + + beforeEach(() => { + loginWithPasswordStub.callsFake((user, password, callback) => callback('User not found')); + rememberMe.loginWithPassword(notExistingUsername, samplePassword); + }); + + it('should not call the updateRememberMe method', () => { + expect(callStub).to.have.callCount(0); + }); + }); + + describe('When login was successful', () => { + beforeEach(() => { + loginWithPasswordStub.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + it('should call the updateRememberMe method', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod); + }); + }); +}); + +describe('Given rememberMe flag', () => { + let callStub, loginWithPasswordStub, rememberMe, rememberMeFlag; + + beforeEach(() => { + rememberMe = new RememberMe(); + loginWithPasswordStub = sinon.stub( + Meteor, + 'loginWithPassword' + ); + callStub = sinon.stub(Meteor, 'call'); + loginWithPasswordStub.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + }); + + afterEach(() => { + loginWithPasswordStub.restore(); + callStub.restore(); + }); + + describe('When rememberMe is not activated on the server', () => { + const warningTitle = 'Dependency meteor/tprzytula:remember-me is not active!\n'; + const warningMessage = '\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.'; + let consoleStub; + + beforeEach(() => { + callStub.callsFake((method, flag, callback) => { + callback({ error: 404 }); + }); + consoleStub = sinon.stub(console, 'warn'); + }); + + afterEach(() => { + consoleStub.restore(); + }); + + describe('And loginWithPassword is invoked', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + it('should inform user about the requirement to activate it on server', () => { + expect(consoleStub).to.have.been.calledWith(warningTitle, warningMessage); + }); + }); + }); + + describe('When rememberMe method fires error', () => { + const errorMessage = 'meteor/tprzytula:remember-me' + + '\nCould not update remember me setting.' + + '\nError:'; + const errorDetails = 'Example details'; + let consoleStub; + + beforeEach(() => { + callStub.callsFake((method, flag, callback) => { + callback(errorDetails); + }); + consoleStub = sinon.stub(console, 'error'); + }); + + afterEach(() => { + consoleStub.restore(); + }); + + describe('And loginWithPassword is invoked', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + it('should inform user about the error', () => { + expect(consoleStub).to.have.been.calledWith(errorMessage, errorDetails); + }); + }); + }); + + describe('When not provided as a parameter (without callback)', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + it('should set remember me to true by default (without callback)', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, true); + }); + }); + + describe('When not provided as a parameter (with callback)', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword, sampleCallback); + }); + + it('should set remember me to true by default (without callback)', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, true); + }); + }); + + describe('When equals "true"', () => { + beforeEach(() => { + rememberMeFlag = true; + }); + + describe('And is passed as a third parameter', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword, rememberMeFlag); + }); + + it('should set rememberMe to true', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, true); + }); + }); + + describe('And is passed as a fourth parameter', () => { + beforeEach(() => { + rememberMe.loginWithPassword( + sampleUsername, + samplePassword, + sampleCallback, + rememberMeFlag + ); + }); + + it('should set rememberMe to true', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, true); + }); + }); + }); + + describe('When equals "false"', () => { + beforeEach(() => { + rememberMeFlag = false; + }); + + describe('And is passed as a third parameter', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword, rememberMeFlag); + }); + + it('should set rememberMe to false', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, false); + }); + }); + + describe('And is passed as a fourth parameter', () => { + beforeEach(() => { + rememberMe.loginWithPassword( + sampleUsername, + samplePassword, + sampleCallback, + rememberMeFlag + ); + }); + + it('should set rememberMe to true', () => { + expect(callStub).to.have.been.calledWith(rememberMeMethod, false); + }); + }); + }); +}); + +describe('Given RememberMe.loginWithPassword method', () => { + let callStub, loginWithPasswordStub, rememberMe; + + beforeEach(() => { + rememberMe = new RememberMe(); + loginWithPasswordStub = sinon.stub(Meteor, 'loginWithPassword'); + callStub = sinon.stub(Meteor, 'call'); + }); + + afterEach(() => { + loginWithPasswordStub.restore(); + callStub.restore(); + }); + + describe('When used with sample parameters', () => { + beforeEach(() => { + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + it('should correctly pass parameters to Meteor.loginWithPassword method', () => { + expect(loginWithPasswordStub).calledWith( + sampleUsername, + samplePassword + ); + }); + }); + + describe('When accounts system is changed', () => { + let connection, remoteAccounts; + + beforeEach(() => { + connection = DDP.connect('127.0.0.1:3000'); + remoteAccounts = new AccountsClient({ connection }); + rememberMe.changeAccountsSystem(remoteAccounts); + }); + + afterEach(() => { + connection.disconnect(); + }); + + describe('And loginWithPassword is used', () => { + let remoteCallStub, remoteLoginWithPasswordStub; + + beforeEach(() => { + remoteLoginWithPasswordStub = sinon.stub( + remoteAccounts.connection, + 'loginWithPassword' + ); + remoteLoginWithPasswordStub.callsFake((user, password, callback) => { + const error = false; + callback(error); + }); + remoteCallStub = sinon.stub(remoteAccounts.connection, 'call'); + rememberMe.loginWithPassword(sampleUsername, samplePassword); + }); + + afterEach(() => { + remoteLoginWithPasswordStub.restore(); + remoteCallStub.restore(); + }); + + it('should use loginWithPassword from the new accounts', () => { + expect(remoteLoginWithPasswordStub).to.have.been.calledWith( + 'username', + 'password' + ); + }); + + it('should call rememberMe method for the connection from the new accounts', () => { + expect(remoteCallStub).to.have.been.calledOnce(); + }); + }); + }); +}); + +describe('Given changeAccountsSystem', () => { + let callStub, rememberMe, result; + + beforeEach(() => { + rememberMe = new RememberMe(); + callStub = sinon.stub(Meteor, 'call'); + }); + + afterEach(() => { + callStub.restore(); + }); + + describe('When an incorrect instance is passed', () => { + beforeEach(() => { + const randomInstance = new RememberMe(); + result = rememberMe.changeAccountsSystem(randomInstance); + }); + + it('should not accept the instance', () => { + expect(result).to.be.equal(false); + }); + }); + + describe('When a correct instance is passed', () => { + let connection, remoteAccounts; + + beforeEach(() => { + connection = DDP.connect('127.0.0.1:3000'); + remoteAccounts = new AccountsClient({ connection }); + result = rememberMe.changeAccountsSystem(remoteAccounts); + }); + + afterEach(() => { + connection.disconnect(); + }); + + it('should accept the instance', () => { + expect(result).to.be.equal(true); + }); + + it('should append "loginWithPassword" method to the instance', () => { + expect('loginWithPassword' in remoteAccounts.connection).to.be.equal(true); + }); + }); + + describe('When passed instance already contains loginWithPassword method', () => { + const loginWithPassword = () => {}; + let connection, remoteAccounts; + + beforeEach(() => { + connection = DDP.connect('127.0.0.1:3000'); + remoteAccounts = new AccountsClient({ connection }); + connection.loginWithPassword = loginWithPassword; + result = rememberMe.changeAccountsSystem(remoteAccounts); + }); + + afterEach(() => { + connection.disconnect(); + }); + + it('should not override the already existing loginWithPassword method', () => { + expect(remoteAccounts.connection.loginWithPassword).to.be.equal(loginWithPassword); + }); + }); }); diff --git a/test/client/overrideLogin.spec.js b/test/client/overrideLogin.spec.js new file mode 100644 index 0000000..79c6a5f --- /dev/null +++ b/test/client/overrideLogin.spec.js @@ -0,0 +1,129 @@ +const { AccountsClient } = require('meteor/accounts-base'); +const overrideLogin = require('./../../client/overrideLogin').default; +const chai = require('ultimate-chai'); +const sinon = require('sinon'); + +const { expect } = chai; + +const isMethodOverridden = 'tprzytula:remember-me_overridden'; +const isCallbackRegistered = 'tprzytula:remember-me_callbackRegistered'; + +describe('Given overrideLogin', () => { + describe('When a new AccountsClient instance is created', () => { + let connection, testAccounts, callLoginMethodStub; + + beforeEach(() => { + connection = DDP.connect('127.0.0.1:3000'); + testAccounts = new AccountsClient({ connection }); + callLoginMethodStub = sinon.stub(testAccounts, 'callLoginMethod'); + }); + + afterEach(() => { + connection.disconnect(); + }); + + it('should not have any callbacks', () => { + const { callbacks } = testAccounts._onLoginHook; + const callbacksAmount = Object.keys(callbacks).length; + expect(callbacksAmount).to.be.equal(0); + }); + + it('should not have set a flag for callback registration', () => { + expect(isCallbackRegistered in testAccounts).to.be.equal(false); + }); + + describe('And it is passed to the overrideLogin method', () => { + beforeEach(() => { + overrideLogin(testAccounts); + }); + + it('should register onLogin callback', () => { + const { callbacks } = testAccounts._onLoginHook; + const callbacksAmount = Object.keys(callbacks).length; + expect(callbacksAmount).to.be.equal(1); + }); + + it('should set callback registration flag', () => { + expect(isCallbackRegistered in testAccounts).to.be.equal(true); + }); + + it('should not override the login method', () => { + expect(isMethodOverridden in testAccounts).to.be.equal(false); + }); + + describe('And registered callback is invoked', () => { + beforeEach(() => { + const { callbacks } = testAccounts._onLoginHook; + callbacks['0'](); + }); + + it('should set login method overridden flag', () => { + expect(isMethodOverridden in testAccounts).to.be.equal(true); + }); + + it('should override the login method', () => { + expect(testAccounts.callLoginMethod).to.not.be.equal(callLoginMethodStub); + }); + + describe('And the callLoginMethod is invoked without method arguments', () => { + beforeEach(() => { + testAccounts.callLoginMethod(); + }); + + it('should also call the original method', () => { + expect(callLoginMethodStub).to.be.calledOnce(); + }); + + it('should create methodArguments with loggedAtLeastOnce argument', () => { + expect(callLoginMethodStub).to.be.calledWith({ + methodArguments: [{ + loggedAtLeastOnce: true + }] + }); + }); + }); + + describe('And the callLoginMethod is invoked with sample method arguments', () => { + const sampleMethodArguments = [ + { user: 'testUser' }, + { password: 'testPassword' } + ]; + + beforeEach(() => { + testAccounts.callLoginMethod({ + methodArguments: [...sampleMethodArguments] + }); + }); + + it('should also call the original method', () => { + expect(callLoginMethodStub).to.be.calledOnce(); + }); + + it('should create methodArguments with loggedAtLeastOnce argument', () => { + const expectedArguments = [ + ...sampleMethodArguments, + { loggedAtLeastOnce: true } + ]; + expect(callLoginMethodStub).to.be.calledWith({ + methodArguments: expectedArguments + }); + }); + }); + }); + + describe('And the same Accounts instance is passed several additional times to the method', () => { + beforeEach(() => { + overrideLogin(testAccounts); + overrideLogin(testAccounts); + overrideLogin(testAccounts); + }); + + it('should not register additional callbacks', () => { + const { callbacks } = testAccounts._onLoginHook; + const callbacksAmount = Object.keys(callbacks).length; + expect(callbacksAmount).to.be.equal(1); + }); + }); + }); + }); +}); diff --git a/test/client/tests/customAccountsClient.js b/test/client/tests/customAccountsClient.js deleted file mode 100644 index ddc95c0..0000000 --- a/test/client/tests/customAccountsClient.js +++ /dev/null @@ -1,130 +0,0 @@ -const { AccountsClient } = require('meteor/accounts-base'); -const RememberMe = require('meteor/tprzytula:remember-me').RememberMeClass; - -const sinon = require('sinon'); -const chai = require('ultimate-chai'); - -const { expect } = chai; - -const rememberMeMethod = 'tprzytula:rememberMe-update'; - -module.exports = () => { - /** - * If the accounts system is being managed not on the - * Meteor server from the main connection then it's required - * to create a new instance of AccountsClient with provided - * DDP connection pointing to the desired server. - */ - describe('Support for custom AccountsClient', () => { - /** - * From the release 0.2.0 a new API method "changeAccountsSystem" - * is provided. Using this method it's possible to switch from - * using the default Accounts from the main Meteor server to - * a custom one. - */ - describe('Changing the default accounts system', () => { - /** - * It's important to only accept instanced of the AccountsClient. - * All invalid parameters should not be accepted, which means - * that the accounts should not change and the method should - * return "false". - */ - it('should not accept invalid AccountsClient', () => { - const rememberMe = new RememberMe(); - const randomInstance = new RememberMe(); - const result = rememberMe.changeAccountsSystem(randomInstance); - expect(result).to.be.equal(false); - }); - - /** - * Upon providing correct AccountsClient instance the method - * should return "true". - */ - it('should switch to new AccountsClient', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - const result = rememberMe.changeAccountsSystem(preparedAccounts); - expect(result).to.be.equal(true); - }); - }); - - describe('Adjusting received AccountsClient', () => { - it('should append "loginWithPassword" method if there is none', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - expect('loginWithPassword' in preparedAccounts.connection).to.be.equal(false); - rememberMe.changeAccountsSystem(preparedAccounts); - expect('loginWithPassword' in preparedAccounts.connection).to.be.equal(true); - }); - }); - - describe('Should start using the passed custom AccountsClient', () => { - /** - * Each time the wrapper is called it - * should also internally call the Meteor's login method. - */ - it('should call loginWithPassword', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - rememberMe.changeAccountsSystem(preparedAccounts); - - const loginWithPassword = sinon.stub( - preparedAccounts.connection, - '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(); - }); - - /** - * 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 rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - rememberMe.changeAccountsSystem(preparedAccounts); - - const loginWithPassword = sinon.stub( - preparedAccounts.connection, - 'loginWithPassword' - ); - const call = sinon.stub(connection, '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(); - }); - }); - }); -}; diff --git a/test/client/tests/method.js b/test/client/tests/method.js deleted file mode 100644 index 3ccbb57..0000000 --- a/test/client/tests/method.js +++ /dev/null @@ -1,128 +0,0 @@ -const RememberMe = require('meteor/tprzytula:remember-me').RememberMeClass; - -const rememberMeMethod = 'tprzytula:rememberMe-update'; -const sinon = require('sinon'); -const chai = require('ultimate-chai'); - -const { expect } = chai; - -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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 = callbackError => callbackError; - 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(); - }); - }); -}; diff --git a/test/client/tests/overrideLogin.js b/test/client/tests/overrideLogin.js deleted file mode 100644 index ba8bb1c..0000000 --- a/test/client/tests/overrideLogin.js +++ /dev/null @@ -1,84 +0,0 @@ -const { AccountsClient } = require('meteor/accounts-base'); -const RememberMe = require('meteor/tprzytula:remember-me').RememberMeClass; - -const isMethodOverridden = 'tprzytula:remember-me_overridden'; -const isCallbackRegistered = 'tprzytula:remember-me_callbackRegistered'; -const chai = require('ultimate-chai'); - -const { expect } = chai; - -module.exports = () => { - /** - * Overriding internal login method after first successful - * login gives us possibility to send another parameter - * "loggedAtLeastOnce" to the every next login request in the - * current application session. This parameter can be recognized - * by server to perform suitable logic. - */ - describe('Overriding internal login method', () => { - /** - * By default freshly created instance of AccountsClient should - * not have any onLogin callbacks set yet. Upon creating - * RememberMe instance and giving to it the preparedAccounts - * a new callback should be registered. - * - * The callback will be used to override the login method - * after only the first successful login attempt during - * the device session. - */ - it('should register onLogin callback on initialization', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - const { callbacks } = preparedAccounts._onLoginHook; - - const beforeInitialization = Object.keys(callbacks).length; - expect(beforeInitialization).to.be.equal(0); - expect(isCallbackRegistered in preparedAccounts).to.be.equal(false); - - rememberMe.changeAccountsSystem(preparedAccounts); - - const afterInitialization = Object.keys(callbacks).length; - expect(afterInitialization).to.be.equal(1); - expect(isCallbackRegistered in preparedAccounts).to.be.equal(true); - }); - - /** - * Given conditions in above tests it's important to - * make sure that the same onLogin callback won't be - * registered/duplicated no matter of how many times we - * will register the same AccountsClient to the RememberMe. - */ - it('should not register more than one callback to the same AccountsClient', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - const { callbacks } = preparedAccounts._onLoginHook; - - rememberMe.changeAccountsSystem(preparedAccounts); - rememberMe.changeAccountsSystem(preparedAccounts); - rememberMe.changeAccountsSystem(preparedAccounts); - rememberMe.changeAccountsSystem(preparedAccounts); - rememberMe.changeAccountsSystem(preparedAccounts); - expect(Object.keys(callbacks).length).to.be.equal(1); - }); - - /** - * After successful change of accounts there should be one - * onLogin callback registered which will have the purpose - * to override the login method. To ensure this it's ensured - * if the method was overwritten only after invoking the callback. - */ - it('should override login method after first successful login', () => { - const rememberMe = new RememberMe(); - const connection = DDP.connect('127.0.0.1:3000'); - const preparedAccounts = new AccountsClient({ connection }); - rememberMe.changeAccountsSystem(preparedAccounts); - expect(isMethodOverridden in preparedAccounts).to.be.equal(false); - - const { callbacks } = preparedAccounts._onLoginHook; - callbacks['0'](); - expect(isMethodOverridden in preparedAccounts).to.be.equal(true); - }); - }); -}; diff --git a/test/client/tests/rememberMe.js b/test/client/tests/rememberMe.js deleted file mode 100644 index bd45285..0000000 --- a/test/client/tests/rememberMe.js +++ /dev/null @@ -1,176 +0,0 @@ -const RememberMe = require('meteor/tprzytula:remember-me').RememberMeClass; - -const rememberMeMethod = 'tprzytula:rememberMe-update'; -const sinon = require('sinon'); -const chai = require('ultimate-chai'); - -const { expect } = chai; - -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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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 rememberMe = new RememberMe(); - 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(); - }); - }); - }); -}; diff --git a/test/server/authenticator.spec.js b/test/server/authenticator.spec.js new file mode 100644 index 0000000..00298b7 --- /dev/null +++ b/test/server/authenticator.spec.js @@ -0,0 +1,144 @@ +const TestUser = require('./utils/testUser'); +const AuthenticatorSpec = require('../../server/authenticator').default; +const LoginAttemptGenerator = require('./utils/loginAttemptGenerator'); +const chai = require('ultimate-chai'); + +const { expect } = chai; +const resume = 'token'; + +describe('Given authenticator', () => { + let result, resultCode, reason; + + describe('When user performs login attempt with loginWithPassword method', () => { + let loginAttempt; + + beforeEach(() => { + const loginAttemptGenerator = new LoginAttemptGenerator({ type: 'password' }); + loginAttempt = loginAttemptGenerator.getLoginAttempt(); + }); + + describe('And the attempt is allowed', () => { + let authenticator; + + beforeEach(() => { + loginAttempt.allowed = true; + authenticator = new AuthenticatorSpec(loginAttempt); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should pass the attempt', () => { + expect(result).to.be.equal(true); + expect(resultCode).to.be.equal(0); + expect(reason).to.be.equal('Validation passed'); + }); + }); + + describe('And the attempt is disallowed', () => { + let authenticator; + + beforeEach(() => { + loginAttempt.allowed = false; + authenticator = new AuthenticatorSpec(loginAttempt); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should not pass the attempt', () => { + expect(result).to.be.equal(false); + expect(resultCode).to.be.equal(-1); + expect(reason).to.be.equal('Attempt disallowed by Meteor'); + }); + }); + }); + + describe('When an resume attempt is received from the client', () => { + let loginAttemptGenerator, testUserInstance; + + beforeEach(() => { + testUserInstance = new TestUser({ + username: 'resume-test', + password: 'resume-test', + }); + testUserInstance.init(); + loginAttemptGenerator = new LoginAttemptGenerator({ + type: 'resume', + user: testUserInstance.getUser() + }); + loginAttemptGenerator.addMethodArgument({ resume }); + }); + + afterEach(() => { + testUserInstance.removeUser(); + }); + + describe('And user was not logged during this device session', () => { + let authenticator; + + beforeEach(() => { + const loginAttempt = loginAttemptGenerator.getLoginAttempt(); + authenticator = new AuthenticatorSpec(loginAttempt); + }); + + describe('And the previous login was with rememberMe', () => { + beforeEach(() => { + testUserInstance.setLoginToken({ resume, rememberMe: true }); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should pass the attempt', () => { + expect(result).to.be.equal(true); + expect(resultCode).to.be.equal(0); + expect(reason).to.be.equal('Validation passed'); + }); + }); + + describe('And the previous login was without rememberMe', () => { + beforeEach(() => { + testUserInstance.setLoginToken({ resume, rememberMe: false }); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should not pass the attempt', () => { + expect(result).to.be.equal(false); + expect(resultCode).to.be.equal(-2); + expect(reason).to.be.equal('Resume not allowed when user does not have remember me'); + }); + }); + }); + + describe('And user was already logged during this device session', () => { + let authenticator; + + beforeEach(() => { + loginAttemptGenerator.addMethodArgument({ loggedAtLeastOnce: true }); + const loginAttempt = loginAttemptGenerator.getLoginAttempt(); + authenticator = new AuthenticatorSpec(loginAttempt); + }); + + describe('And the previous login was with rememberMe', () => { + beforeEach(() => { + testUserInstance.setLoginToken({ resume, rememberMe: true }); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should pass the attempt', () => { + expect(result).to.be.equal(true); + expect(resultCode).to.be.equal(0); + expect(reason).to.be.equal('Validation passed'); + }); + }); + + describe('And the previous login was without rememberMe', () => { + beforeEach(() => { + testUserInstance.setLoginToken({ resume, rememberMe: false }); + ({ result, resultCode, reason } = authenticator.validateAttempt()); + }); + + it('should pass the attempt', () => { + expect(result).to.be.equal(true); + expect(resultCode).to.be.equal(0); + expect(reason).to.be.equal('Validation passed'); + }); + }); + }); + }); +}); diff --git a/test/server/index.spec.js b/test/server/index.spec.js deleted file mode 100644 index 338d568..0000000 --- a/test/server/index.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -const login = require('./tests/login'); -const method = require('./tests/method'); -const resume = require('./tests/resume'); - -/** - * Server-side test cases. - */ -describe('server', () => { - login(); - method(); - resume(); -}); diff --git a/test/server/method.spec.js b/test/server/method.spec.js new file mode 100644 index 0000000..2904138 --- /dev/null +++ b/test/server/method.spec.js @@ -0,0 +1,36 @@ +const RememberMe = require('meteor/tprzytula:remember-me'); + +const rememberMeMethod = 'tprzytula:rememberMe-update'; +const chai = require('ultimate-chai'); + +const { expect } = chai; +const getMeteorMethods = () => Meteor.default_server.method_handlers; +const checkIfMeteorMethodExists = name => name in getMeteorMethods(); + +describe('Given a rememberMe method', () => { + /** + * Having this dependency installed should not invoke it. + * The rememberMe method used for the communication client <> server + * should not exist. + */ + it('should not exist by default', () => { + const doesExist = checkIfMeteorMethodExists(rememberMeMethod); + expect(doesExist).to.be.equal(false); + }); + + describe('When rememberMe functionality is activated', () => { + beforeEach(() => { + RememberMe.activate(); + }); + + /** + * After activating the functionality a new meteor method + * should be created. From now user is able to invoke the + * 'tprzytula:rememberMe-update' method. + */ + it('should exist after activating the functionality', () => { + const doesExist = checkIfMeteorMethodExists(rememberMeMethod); + expect(doesExist).to.be.equal(true); + }); + }); +}); diff --git a/test/server/tests/login.js b/test/server/tests/login.js deleted file mode 100644 index b56b362..0000000 --- a/test/server/tests/login.js +++ /dev/null @@ -1,50 +0,0 @@ -const Authenticator = require('../../../server/authenticator').default; -const LoginAttemptGenerator = require('../utils/loginAttemptGenerator'); -const chai = require('ultimate-chai'); - -const { expect } = chai; -const type = 'password'; - -module.exports = () => { - /** - * Those tests are covering a basic user login attempt. - * The attempt is invoked if the user is logging for the first - * time by using methods such as Meteor.loginWithPassword. - */ - describe('login attempt', () => { - /** - * The attempt can be disallowed already from the previously - * ran validators. It can be a validator directly from the Meteor core - * saying that the password is wrong but also another one created by the developer. - * - * In this case there is no need to validate the attempt anymore. - * It should be instantly disallowed again. - */ - it('should not pass if the attempt is already disallowed', () => { - const loginAttemptGenerator = new LoginAttemptGenerator({ type }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - loginAttempt.allowed = false; - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(false); - expect(resultCode).to.be.equal(-1); - expect(reason).to.be.equal('Attempt disallowed by Meteor'); - }); - - /** - * The dependency logic should not affect normal login attempts. - * Because of that if the previously ran validations succeeded the - * dependency should also let it pass further. - */ - it('should pass if the attempt is allowed', () => { - const loginAttemptGenerator = new LoginAttemptGenerator({ type }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - loginAttempt.allowed = true; - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(true); - expect(resultCode).to.be.equal(0); - expect(reason).to.be.equal('Validation passed'); - }); - }); -}; diff --git a/test/server/tests/method.js b/test/server/tests/method.js deleted file mode 100644 index f6530e7..0000000 --- a/test/server/tests/method.js +++ /dev/null @@ -1,41 +0,0 @@ -const RememberMe = require('meteor/tprzytula:remember-me'); - -const rememberMeMethod = 'tprzytula:rememberMe-update'; -const chai = require('ultimate-chai'); - -const { expect } = chai; -const getMeteorMethods = () => Meteor.default_server.method_handlers; -const checkIfMeteorMethodExists = name => name in getMeteorMethods(); - -module.exports = () => { - /** - * The dependency is significantly affecting how the login system works. - * I'm against the idea of dependencies which are running automatically - * without the developer knowledge. There are situations where developers - * are leaving not used dependencies in the list. In this case it can be - * hard for them to debug and find the reason for their login system - * to work differently than the normal one should. - */ - describe('remember-me method', () => { - /** - * Having this dependency installed should not invoke it. - * The main method used for the communication client <> server - * should not exist. - */ - it('should not exist by default', () => { - const doesExist = checkIfMeteorMethodExists(rememberMeMethod); - expect(doesExist).to.be.equal(false); - }); - - /** - * After activating the functionality a new meteor method - * should be created. From now all users are being able to - * invoke the 'tprzytula:rememberMe-update' method. - */ - it('should exist after activating the functionality', () => { - RememberMe.activate(); - const doesExist = checkIfMeteorMethodExists(rememberMeMethod); - expect(doesExist).to.be.equal(true); - }); - }); -}; diff --git a/test/server/tests/resume.js b/test/server/tests/resume.js deleted file mode 100644 index 0596656..0000000 --- a/test/server/tests/resume.js +++ /dev/null @@ -1,105 +0,0 @@ -const TestUser = require('../utils/testUser'); -const Authenticator = require('../../../server/authenticator').default; -const LoginAttemptGenerator = require('../utils/loginAttemptGenerator'); -const chai = require('ultimate-chai'); - -const { expect } = chai; -const type = 'resume'; -const resume = 'token'; -const testUser = new TestUser({ - username: 'resume-test', - password: 'resume-test', -}); - -module.exports = () => { - /** - * Those tests are covering the autologin attempt. - * The attempt is invoked by the core of the meteor accounts. - * Every time a previously logged in user reconnects to the system - * there is a "resume" attempt sent. - * - * This dependency did allow the user to decide during the login - * if he want to have the rememberMe flag set on true or false. - * This setting will have an importance of the decision being made during resume. - */ - describe('resume attempt', () => { - /** - * In case of an user logging in with rememberMe the resume - * attempt should be allowed. This covers a situation where user - * is being logged with rememberMe and then restarts the application. - * The user should stay logged in. - */ - it('should pass if user does have rememberMe', () => { - testUser.setLoginToken({ resume, rememberMe: true }); - const loginAttemptGenerator = new LoginAttemptGenerator({ type, resume }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(true); - expect(resultCode).to.be.equal(0); - expect(reason).to.be.equal('Validation passed'); - }); - - /** - * In case of an user logging in without rememberMe the resume - * attempt should not be allowed. This covers a situation where user - * is being logged without rememberMe and then restarts the application. - * The user should be logged out. - */ - it('should not pass if user does not have rememberMe', () => { - testUser.setLoginToken({ resume, rememberMe: false }); - const loginAttemptGenerator = new LoginAttemptGenerator({ type, resume }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(false); - expect(resultCode).to.be.equal(-2); - expect(reason).to.be.equal('Resume not allowed when user does not have remember me'); - }); - - /** - * Important thing to keep in mind is that Meteor's login system does not know - * when the user is starting the app from the scratch or just lost the internet. - * It's not intended to logout an user without rememberMe every time he will lose - * the internet connection. - * - * To avoid this situation the user from now is sending also 'loggedAtLeastOnce: true' - * flag if he already logged once in ongoing device session. - */ - describe('connection loss', () => { - /** - * If the user already had a successfull login attempt during his device session - * then he should stay logged in no matter the rememberMe setting after the reconnect. - * Validates if user stays online with rememberMe being set to 'false'. - */ - it('should pass for the same session as previous login when without rememberMe', () => { - testUser.setLoginToken({ resume, rememberMe: false }); - const loginAttemptGenerator = new LoginAttemptGenerator({ type, resume }); - loginAttemptGenerator.addMethodArgument({ loggedAtLeastOnce: true }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(true); - expect(resultCode).to.be.equal(0); - expect(reason).to.be.equal('Validation passed'); - }); - - /** - * If the user already had a successfull login attempt during his device session - * then he should stay logged in no matter the rememberMe setting after the reconnect. - * Validates if user stays online with rememberMe being set to 'true'. - */ - it('should pass for the same session as previous login when with rememberMe', () => { - testUser.setLoginToken({ resume, rememberMe: true }); - const loginAttemptGenerator = new LoginAttemptGenerator({ type, resume }); - loginAttemptGenerator.addMethodArgument({ loggedAtLeastOnce: true }); - const loginAttempt = loginAttemptGenerator.getLoginAttempt(); - const authenticator = new Authenticator(loginAttempt); - const { result, resultCode, reason } = authenticator.validateAttempt(); - expect(result).to.be.equal(true); - expect(resultCode).to.be.equal(0); - expect(reason).to.be.equal('Validation passed'); - }); - }); - }); -}; diff --git a/test/server/utils/loginAttemptGenerator.js b/test/server/utils/loginAttemptGenerator.js index aa36a94..daeea58 100644 --- a/test/server/utils/loginAttemptGenerator.js +++ b/test/server/utils/loginAttemptGenerator.js @@ -15,9 +15,13 @@ class LoginAttemptGenerator { /** * Creates loginAttempt object according to the provided options. - * @param {*} param0 + * @param {Object} configuration */ - createAttempt({ type = 'password', allowed = true, resume }) { + createAttempt({ + type = 'password', + allowed = true, + user + }) { this.loginAttempt = { type, allowed, @@ -28,11 +32,7 @@ class LoginAttemptGenerator { }; if (type === 'resume') { - this.loginAttempt.user = Meteor.users.findOne(); - } - - if (resume) { - this.loginAttempt.methodArguments.push({ resume }); + this.loginAttempt.user = user || Meteor.users.findOne(); } } diff --git a/test/server/utils/testUser.js b/test/server/utils/testUser.js index 7b85526..ffb681c 100644 --- a/test/server/utils/testUser.js +++ b/test/server/utils/testUser.js @@ -12,17 +12,23 @@ class TestUser { constructor(options = {}) { this.username = options.username || 'test'; this.password = options.password || 'test'; - this.init(); } /** * Invoked at the class initialization. */ init() { - this.removeUser(); this.createUser(); } + /** + * Returns user document from the collection. + * @returns {Object} user + */ + getUser() { + return Meteor.users.findOne({ username: this.username }); + } + /** * Removes user with the same username if exists. */