Skip to content

Commit

Permalink
Pass options into server-side log-in lifecycle hooks
Browse files Browse the repository at this point in the history
* Provides server-side parity with the work started in #11913
* Attaches any `options` provided in the result of the log-in handler to the `attempt`
* Adds a small suite of tests covering client-side and server-side places where `options` is expected to be available
  • Loading branch information
nazrhyn committed Apr 12, 2024
1 parent 5c3b3c5 commit 4677819
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 9 deletions.
70 changes: 70 additions & 0 deletions packages/accounts-base/accounts_login_options_client_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';

const LOGIN_TYPE = '__test';

Tinytest.addAsync(
'accounts - login - successful attempt with options',
async test => {
// --SETUP--
const testUserId = await Meteor.callAsync('registerTestLoginHandler');
const validateResultHandler = new MockFunction();
const onLoginHandler = new MockFunction();
const onLoginHook = Accounts.onLogin(onLoginHandler);

// --TEST--
const result = await callLoginMethodAsync({
methodArguments: [{ [LOGIN_TYPE]: { userId: testUserId } }],
validateResult: validateResultHandler,
});

// `result` is the `userCallback` result.
expectResult(test, result, testUserId);

// Verify the `validateResult` parameter.
const validateResultCallsCount = validateResultHandler.mock.calls.length;
test.equal(validateResultCallsCount, 1);

if (validateResultCallsCount === 1) {
const [result] = validateResultHandler.mock.calls[0];
expectResult(test, result, testUserId);
}

// Verify the `onLogin` parameter.
const onLoginHandlerCallsCount = onLoginHandler.mock.calls.length;
test.equal(onLoginHandlerCallsCount, 1);

if (onLoginHandlerCallsCount === 1) {
const [result] = onLoginHandler.mock.calls[0];
expectResult(test, result, testUserId);
}

// --TEARDOWN--
onLoginHook.stop();
await Meteor.callAsync('removeTestLoginHandler', testUserId);
},
);

function callLoginMethodAsync({ methodArguments, validateResult }) {
return new Promise((resolve, reject) => {
Accounts.callLoginMethod({
methodArguments,
validateResult(result) {
validateResult?.(result);
},
userCallback(error, result) {
if (error) {
reject(error);
} else {
resolve(result);
}
},
});
});
}

function expectResult(test, result, userId) {
test.equal(result.type, LOGIN_TYPE);
test.equal(result.id, userId);
test.equal(result.foo, 'bar'); // comes from `options`
}
200 changes: 200 additions & 0 deletions packages/accounts-base/accounts_login_options_server_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';

const LOGIN_TYPE = '__test';

/**
* Registers a test log-in handler.
* @param {(object) => object} getResult A function that returns the desired results, given the options.
*/
export function registerTestLoginHandler(getResult) {
Accounts.registerLoginHandler(LOGIN_TYPE, options => {
if (!options[LOGIN_TYPE]) return;
return getResult(options[LOGIN_TYPE]);
});
}

/**
* Removes the test log-in handler.
*/
export function removeTestLoginHandler() {
Accounts._loginHandlers = Accounts._loginHandlers.filter(h => h.name !== LOGIN_TYPE);
}

Tinytest.add(
'accounts - login - successful attempt with options',
test => {
// --SETUP--
const userId = Accounts.insertUserDoc({});
registerTestLoginHandler(() => ({
userId,
options: { foo: 'bar' },
}));

const hooks = registerLifecycleHooks();
const {
validateLoginAttemptHandler,
onLoginHandler,
onLoginFailureHandler,
} = hooks;

// --TEST--
const conn = DDP.connect(Meteor.absoluteUrl());
const result = conn.call('login', { [LOGIN_TYPE]: true });

test.equal(result.id, userId);
test.equal(result.type, LOGIN_TYPE);
test.equal(result.foo, 'bar');

expectHandlerCalledWithAttempt(test, validateLoginAttemptHandler, { allowed: true })
expectHandlerCalledWithAttempt(test, onLoginHandler, { allowed: true });

test.length(onLoginFailureHandler.mock.calls, 0);

// --TEARDOWN--
conn.call('logout');
conn.disconnect();

hooks.stop();

Meteor.users.remove(userId);
removeTestLoginHandler();
},
);

Tinytest.add(
'accounts - login - failed attempt with options and no user',
test => {
// --SETUP--
registerTestLoginHandler(() => ({
error: new Meteor.Error('log-in-error'),
options: { foo: 'bar' },
}));

const hooks = registerLifecycleHooks();
const {
validateLoginAttemptHandler,
onLoginHandler,
onLoginFailureHandler,
} = hooks;

// --TEST--
const conn = DDP.connect(Meteor.absoluteUrl());
test.throws(
() => conn.call('login', { [LOGIN_TYPE]: true }),
'log-in-error',
);

expectHandlerCalledWithAttempt(test, validateLoginAttemptHandler, { allowed: false });
expectHandlerCalledWithAttempt(test, onLoginFailureHandler, { allowed: false });

test.length(onLoginHandler.mock.calls, 0);

// --TEARDOWN--
conn.call('logout');
conn.disconnect();

hooks.stop();

removeTestLoginHandler();
},
);

Tinytest.add(
'accounts - login - failed attempt with options and a user',
test => {
// --SETUP--
const userId = Accounts.insertUserDoc({});
registerTestLoginHandler(() => ({
error: new Meteor.Error('log-in-error'),
userId,
options: { foo: 'bar' },
}));

const hooks = registerLifecycleHooks();
const {
validateLoginAttemptHandler,
onLoginHandler,
onLoginFailureHandler,
} = hooks;

// --TEST--
const conn = DDP.connect(Meteor.absoluteUrl());
test.throws(
() => conn.call('login', { [LOGIN_TYPE]: true }),
'log-in-error',
);

expectHandlerCalledWithAttempt(test, validateLoginAttemptHandler, { allowed: false, hasUser: true });
expectHandlerCalledWithAttempt(test, onLoginFailureHandler, { allowed: false, hasUser: true });

test.length(onLoginHandler.mock.calls, 0);

// --TEARDOWN--
conn.call('logout');
conn.disconnect();

hooks.stop();

Meteor.users.remove(userId);
removeTestLoginHandler();
},
);

function registerLifecycleHooks() {
const validateLoginAttemptHandler = new MockFunction(() => true);
const validateLoginAttemptHook = Accounts.validateLoginAttempt(validateLoginAttemptHandler);

const onLoginHandler = new MockFunction();
const onLoginHook = Accounts.onLogin(onLoginHandler);

const onLoginFailureHandler = new MockFunction();
const onLoginFailureHook = Accounts.onLoginFailure(onLoginFailureHandler);

return {
validateLoginAttemptHandler,
onLoginHandler,
onLoginFailureHandler,
stop() {
validateLoginAttemptHook.stop();
onLoginHook.stop();
onLoginFailureHook.stop();
},
};
}


function expectHandlerCalledWithAttempt(test, handler, { allowed, hasUser }) {
const callCount = handler.mock.calls.length;
test.equal(callCount, 1);

if (callCount === 1) {
const [ attempt ] = handler.mock.calls[0];
expectLoginAttempt(test, attempt, { allowed, hasUser });
}
}

function expectLoginAttempt(test, attempt, { allowed, hasUser = allowed }) {
test.isTrue(attempt);

if (!attempt) return;

test.equal(attempt.type, LOGIN_TYPE);
test.equal(attempt.allowed, allowed);
test.equal(attempt.methodName, 'login');

if (allowed) {
test.isFalse(attempt.error);
} else {
test.isTrue(attempt.error);
}

if (hasUser) {
test.isTrue(attempt.user);
} else {
test.isFalse(attempt.user);
}

test.isTrue(attempt.options);
test.equal(attempt.options.foo, 'bar');
}
4 changes: 3 additions & 1 deletion packages/accounts-base/accounts_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ export class AccountsServer extends AccountsCommon {
if (result.error) {
attempt.error = result.error;
}
if (result.options) {
attempt.options = result.options;
}
if (user) {
attempt.user = user;
}
Expand Down Expand Up @@ -1818,4 +1821,3 @@ const generateCasePermutationsForString = string => {
}
return permutations;
}

18 changes: 18 additions & 0 deletions packages/accounts-base/accounts_tests_setup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { Accounts } from 'meteor/accounts-base';

import { registerTestLoginHandler, removeTestLoginHandler } from './accounts_login_options_server_tests';

const getTokenFromSecret = ({ selector, secret: secretParam }) => {
let secret = secretParam;

Expand Down Expand Up @@ -33,4 +37,18 @@ Meteor.methods({
return getTokenFromSecret({ selector, secret });
},
getTokenFromSecret,
// Helpers for `accounts_login_options_client_tests.js`
registerTestLoginHandler() {
registerTestLoginHandler(({ userId }) => ({
userId,
options: { foo: 'bar' },
}));
// Insert a test user so the client doesn't have to deal with it.
return Accounts.insertUserDoc({});
},
removeTestLoginHandler(userId) {
removeTestLoginHandler();
// Remove the test user.
Meteor.users.remove(userId);
}
});
1 change: 1 addition & 0 deletions packages/accounts-base/client_tests.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./accounts_url_tests.js";
import "./accounts_reconnect_tests.js";
import "./accounts_client_tests.js";
import "./accounts_login_options_client_tests.js";
2 changes: 1 addition & 1 deletion packages/accounts-base/package.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package.describe({
summary: 'A user account system',
version: '2.2.10',
version: '2.2.11',
});

Package.onUse(api => {
Expand Down
1 change: 1 addition & 0 deletions packages/accounts-base/server_tests.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import "./accounts_tests.js";
import "./accounts_reconnect_tests.js";
import './accounts_login_options_server_tests.js';
23 changes: 23 additions & 0 deletions packages/test-helpers/mock_function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* A simple call-recording mock function.
* @type {MockFunction}
*/
MockFunction = class MockFunction {
calls = [];

constructor(fn = () => {}) {
const self = this;
const mocked = function (...args) {
self.calls.push(args);
return fn.call(this, ...args);
};

mocked.mock = this;

return mocked;
}

reset() {
this.calls.length = 0;
}
}
27 changes: 20 additions & 7 deletions packages/test-helpers/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,25 @@ Package.onUse(function (api) {


api.export([
'pollUntil', 'try_all_permutations',
'SeededRandom', 'clickElement', 'blurElement',
'focusElement', 'simulateEvent', 'getStyleProperty', 'canonicalizeHtml',
'renderToDiv', 'clickIt',
'withCallbackLogger', 'testAsyncMulti',
'simplePoll', 'runAndThrowIfNeeded',
'makeTestConnection', 'DomUtils']);
'blurElement',
'canonicalizeHtml',
'clickElement',
'clickIt',
'DomUtils',
'focusElement',
'getStyleProperty',
'makeTestConnection',
'MockFunction',
'pollUntil',
'renderToDiv',
'runAndThrowIfNeeded',
'SeededRandom',
'simplePoll',
'simulateEvent',
'testAsyncMulti',
'try_all_permutations',
'withCallbackLogger',
]);

api.addFiles('try_all_permutations.js');
api.addFiles('async_multi.js');
Expand All @@ -40,6 +52,7 @@ Package.onUse(function (api) {
api.addFiles('render_div.js');
api.addFiles('current_style.js');
api.addFiles('callback_logger.js');
api.addFiles('mock_function.js');
api.addFiles('domutils.js', 'client');
api.addFiles('connection.js', 'server');
});
Expand Down

0 comments on commit 4677819

Please sign in to comment.