Merge branch 'meteor-accounts-ux-pass' into release-0.7.1

This commit is contained in:
Emily Stark
2014-02-20 00:00:16 -08:00
14 changed files with 463 additions and 145 deletions

View File

@@ -161,6 +161,7 @@
* Don't cache direct references to the fields arguments to the subscription
`added` and `changed` methods. #1750
## v0.7.0.1
* Two fixes to `meteor run` Mongo startup bugs that could lead to hangs with the

View File

@@ -62,7 +62,7 @@ var openCenteredPopup = function(url, width, height) {
return newwindow;
};
// XXX COMPAT WITH 0.6.6.3
// XXX COMPAT WITH 0.7.0.1
// Private interface but probably used by many oauth clients in atmosphere.
Oauth.initiateLogin = function (credentialToken, url, callback, dimensions) {
Oauth.showPopup(

View File

@@ -50,25 +50,41 @@ var withAccountsConnection = function (f) {
};
};
// Open a DDP connection to the accounts server, log in using the
// provided token, and ensure that the connection stays logged in across
// reconnects.
// Open a DDP connection to the accounts server and log in using the
// provided token. Returns the connection, or null if login fails.
//
// XXX if we reconnect we won't reauthenticate. Fix that before using
// this for long-lived connections.
var loggedInAccountsConnection = function (token) {
var connection = getLoadedPackages().livedata.DDP.connect(
config.getAuthDDPUrl()
);
var onReconnect = function () {
connection.apply(
'login',
[{ resume: token }],
{ wait: true },
function (err, result) {
if (err)
throw err;
}
);
};
onReconnect();
var fut = new Future;
connection.apply(
'login',
[{ resume: token }],
{ wait: true },
function (err, result) {
fut['return']({ err: err, result: result });
}
);
var outcome = fut.wait();
if (outcome.err) {
connection.close();
if (outcome.err.error === 403) {
// This is not an ideal value for the error code, but it means
// "server rejected our access token". For example, it expired
// or we revoked it from the web.
return null;
}
// Something else went wrong
throw outcome.err;
}
return connection;
};
@@ -189,26 +205,33 @@ var writeMeteorAccountsUsername = function (username) {
// Given an object 'data' in the format returned by readSessionData,
// modify it to make the user logged out.
var logOutAllSessions = function (data) {
_.each(data.sessions, function (session, domain) {
logOutSession(session);
});
};
// As logOutAllSessions, but for a session on a particular domain
// rather than all sessions.
var logOutSession = function (session) {
var crypto = require('crypto');
_.each(data.sessions, function (session, domain) {
delete session.username;
delete session.userId;
delete session.username;
delete session.userId;
delete session.registrationUrl;
if (_.has(session, 'token')) {
if (! (session.pendingRevoke instanceof Array))
session.pendingRevoke = [];
if (_.has(session, 'token')) {
if (! (session.pendingRevoke instanceof Array))
session.pendingRevoke = [];
// Delete the auth token itself, but save the tokenId, which
// is useless for authentication. The next time we're online,
// we'll send the tokenId to the server to revoke the token on
// the server side too.
if (typeof session.tokenId === "string")
session.pendingRevoke.push(session.tokenId);
delete session.token;
delete session.tokenId;
}
});
// Delete the auth token itself, but save the tokenId, which is
// useless for authentication. The next time we're online, we'll
// send the tokenId to the server to revoke the token on the
// server side too.
if (typeof session.tokenId === "string")
session.pendingRevoke.push(session.tokenId);
delete session.token;
delete session.tokenId;
}
};
// Given an object 'data' in the format returned by readSessionData,
@@ -222,37 +245,7 @@ var loggedIn = function (data) {
// the logged in user doesn't have a username.
var currentUsername = function (data) {
var sessionData = getSession(data, config.getAccountsDomain());
if (sessionData.username) {
return sessionData.username;
} else if (loggedIn(data) && sessionData.token) {
// If it looks like we are logged in but we don't yet have a
// username, then ask the server if we have one.
var username = null;
var fut = new Future();
var connection = loggedInAccountsConnection(sessionData.token);
connection.call('getUsername', function (err, result) {
if (err) {
// If anything went wrong, return null just as we would have if
// we hadn't bothered to ask the server.
fut['return'](null);
return;
}
fut['return'](result);
});
setTimeout(inFiber(function () {
fut['return'](null);
}), 5000);
username = fut.wait();
connection.close();
if (username) {
writeMeteorAccountsUsername(username);
}
return username;
} else {
return null;
}
return sessionData.username || null;
};
var removePendingRevoke = function (domain, tokenIds) {
@@ -584,13 +577,18 @@ var doInteractivePasswordLogin = function (options) {
return true;
};
// options are the same as for doInteractivePasswordLogin, exept without
// options are the same as for doInteractivePasswordLogin, except without
// username and email.
exports.doUsernamePasswordLogin = function (options) {
var username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
});
var username;
do {
username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
}).trim();
} while (username.length === 0);
return doInteractivePasswordLogin(_.extend({}, options, {
username: username
}));
@@ -685,9 +683,71 @@ exports.logoutCommand = function (options) {
process.stderr.write("Not logged in.\n");
};
exports.currentUsername = function () {
// If this is fully set up account (with a username and password), or
// if not logged in, do nothing. If it is an account without a
// username, contact the server and see if the user finished setting
// it up, and if so grab and save the username. But contact the server
// only once per run of the program. Options:
// - noLogout: boolean. Set to true if you don't want this function to
// log out the session if wehave an invalid credential (for example,
// if a caller wants to do its own error handling for invalid
// credentials). Defaults to false.
var alreadyPolledForRegistration = false;
exports.pollForRegistrationCompletion = function (options) {
if (alreadyPolledForRegistration)
return;
alreadyPolledForRegistration = true;
options = options || {};
var data = readSessionData();
return currentUsername(data);
var session = getSession(data, config.getAccountsDomain());
if (session.username || ! session.token)
return;
// We are logged in but we don't yet have a username. Ask the server
// if a username was chosen since we last checked.
var username = null;
var fut = new Future();
var connection = loggedInAccountsConnection(session.token);
if (! connection) {
// Server says our credential isn't any good anymore! Get rid of
// it. Note that, out of an abundance of caution, this also will
// enqueue the credential for invalidation (on a future run, we
// will try to explicitly revoke the credential ourselves).
if (! options.noLogout) {
logOutSession(session);
writeSessionData(data);
}
return;
}
connection.call('getUsername', function (err, result) {
if (fut.isResolved())
return;
if (err) {
// If anything went wrong, return null just as we would have if
// we hadn't bothered to ask the server.
fut['return'](null);
return;
}
fut['return'](result);
});
var timer = setTimeout(inFiber(function () {
if (! fut.isResolved()) {
fut['return'](null);
}
}), 5000);
username = fut.wait();
connection.close();
clearTimeout(timer);
if (username) {
writeMeteorAccountsUsername(username);
}
};
exports.registrationUrl = function () {
@@ -698,6 +758,7 @@ exports.registrationUrl = function () {
exports.whoAmICommand = function (options) {
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var data = readSessionData();
if (! loggedIn(data)) {
@@ -775,11 +836,23 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
} else if (result.alreadyExisted && result.sentRegistrationEmail) {
process.stderr.write(
"\n" +
"That email address is already in use. We need to confirm that it belongs\n" +
"to you. Luckily this will only take a moment.\n" +
"\n" +
"Check your mail! We've sent you a link. Click it, pick a password,\n" +
"and then come back here to deploy your app.\n");
"You need to pick a password for your account so that you can log in.\n" +
"An email has been sent to you with the link.\n\n");
var animationFrame = 0;
var lastLinePrinted = "";
var timer = setInterval(function () {
var spinner = ['-', '\\', '|', '/'];
lastLinePrinted = "Waiting for you to register on the web... " +
spinner[animationFrame];
process.stderr.write(lastLinePrinted + "\r");
animationFrame = (animationFrame + 1) % spinner.length;
}, 200);
var stopSpinner = function () {
process.stderr.write(new Array(lastLinePrinted.length + 1).join(' ') +
"\r");
clearInterval(timer);
};
try {
var waitForRegistrationResult = connection.call(
@@ -787,17 +860,17 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
email
);
} catch (e) {
stopSpinner();
if (! (e instanceof getLoadedPackages().meteor.Meteor.Error))
throw e;
process.stderr.write(
"\nWhen you've picked your password, run 'meteor login' and then you'll\n" +
"be good to go.\n");
"When you've picked your password, run 'meteor login' to log in.\n")
return false;
}
process.stderr.write("\nGreat! Nice to meet you, " +
waitForRegistrationResult.username +
"! Now log in with your new password.\n");
stopSpinner();
process.stderr.write("Username: " +
waitForRegistrationResult.username + "\n");
loginResult = doInteractivePasswordLogin({
username: waitForRegistrationResult.username,
retry: true,
@@ -821,6 +894,34 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
}
});
// options: firstTime, leadingNewline
exports.maybePrintRegistrationLink = function (options) {
options = options || {};
auth.pollForRegistrationCompletion();
var data = readSessionData();
var session = getSession(data, config.getAccountsDomain());
if (session.userId && ! session.username && session.registrationUrl) {
if (options.leadingNewline)
process.stderr.write("\n");
if (! options.firstTime) {
// If they've already been prompted to set a password then this
// is more of a friendly reminder, so we word it slightly
// differently than the first time they're being shown a
// registration url.
process.stderr.write(
"You should set a password on your Meteor developer account. It takes\n" +
"about a minute at: " + session.registrationUrl + "\n\n");
} else {
process.stderr.write(
"You can set a password on your account or change your email address at:\n" +
session.registrationUrl + "\n\n");
}
}
};
exports.tryRevokeOldTokens = tryRevokeOldTokens;
exports.getSessionId = function (domain) {

View File

@@ -678,6 +678,7 @@ main.registerCommand({
}
}, function (options) {
var mongoUrl;
var usedMeteorAccount = false;
if (options.args.length === 0) {
// localhost mode
@@ -708,6 +709,7 @@ main.registerCommand({
mongoUrl = deployGalaxy.temporaryMongoUrl(site);
} else {
mongoUrl = deploy.temporaryMongoUrl(site);
usedMeteorAccount = true;
}
if (! mongoUrl)
@@ -717,6 +719,8 @@ main.registerCommand({
if (options.url) {
console.log(mongoUrl);
} else {
if (usedMeteorAccount)
auth.maybePrintRegistrationLink();
process.stdin.pause();
var runMongo = require('./run-mongo.js');
runMongo.runMongoShell(mongoUrl);
@@ -862,26 +866,16 @@ main.registerCommand({
});
}
var registrationUrl = auth.registrationUrl();
if (registrationUrl &&
deployResult === 0 &&
! auth.currentUsername()) {
process.stderr.write("\n");
if (loggedIn) {
if (deployResult === 0) {
auth.maybePrintRegistrationLink({
leadingNewline: true,
// If the user was already logged in at the beginning of the
// deploy, then they've already been prompted to set a password
// and this is more of a friendly reminder to set their password,
// so we word it slightly differently than the first time they're
// being shown a registration url.
process.stderr.write(
"You should set a password on your Meteor developer account. It takes\n" +
"about a minute at: " + registrationUrl + "\n\n");
} else {
process.stderr.write(
"You can set a password on your account or change your email address at:\n" +
registrationUrl + "\n\n");
}
// at least once before, so we use a slightly different message.
firstTime: ! loggedIn
});
}
return deployResult;
});
@@ -942,6 +936,7 @@ main.registerCommand({
}
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (hostedWithGalaxy(site)) {
@@ -976,15 +971,14 @@ main.registerCommand({
maxArgs: 1
}, function (options) {
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (! auth.isLoggedIn()) {
// XXX meteor.com/create-account or something should have a nice
// registration form
process.stderr.write(
"\nYou must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, you can quickly\n" +
"create one at www.meteor.com.\n\n");
"You must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, create one by clicking\n" +
"'Sign in' and then 'Create account' at www.meteor.com.\n\n");
return 1;
}
@@ -1202,10 +1196,14 @@ main.registerCommand({
name: 'login',
options: {
email: { type: String },
// Undocumented: get credentials on a specific Galaxy. Do we still
// need this?
galaxy: { type: String }
}
}, function (options) {
return auth.loginCommand(options);
return auth.loginCommand(_.extend({
overwriteExistingToken: true
}, options));
});
@@ -1252,12 +1250,13 @@ main.registerCommand({
main.registerCommand({
name: 'self-test',
minArgs: 0,
maxArgs: 1,
options: {
changed: { type: Boolean },
'force-online': { type: Boolean },
slow: { type: Boolean },
history: { type: Number },
tests: { type: String }
},
hidden: true
}, function (options) {
@@ -1275,13 +1274,13 @@ main.registerCommand({
}
var testRegexp = undefined;
if (options.tests) {
if (options.args.length) {
try {
testRegexp = new RegExp(options.tests);
testRegexp = new RegExp(options.args[0]);
} catch (e) {
if (!(e instanceof SyntaxError))
throw e;
process.stderr.write("Bad regular expression: " + options.tests + "\n");
process.stderr.write("Bad regular expression: " + options.args[0] + "\n");
return 1;
}
}

View File

@@ -150,7 +150,7 @@ _.extend(ServiceConnection.prototype, {
}
});
self.subscribe.apply(self, args);
self.connection.subscribe.apply(self.connection, args);
return fut.wait();
},

View File

@@ -147,10 +147,13 @@ var authedRpc = function (options) {
expectPayload: []
});
if (infoResult.statusCode === 403 && rpcOptions.promptIfAuthFails) {
if (infoResult.statusCode === 401 && rpcOptions.promptIfAuthFails) {
// Our authentication didn't validate, so prompt the user to log in
// again, and resend the RPC if the login succeeds.
var username = utils.readLine({ prompt: "Username: " });
var username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
});
var loginOptions = {
username: username,
suppressErrorMessage: true
@@ -262,9 +265,9 @@ var printLegacyPasswordMessage = function (site) {
// authorized for, instruct them to get added via 'meteor authorized
// --add' or switch accounts.
var printUnauthorizedMessage = function () {
var username = auth.currentUsername();
var username = auth.loggedInUsername();
process.stderr.write(
"\nSorry, that site belongs to a different user.\n" +
"Sorry, that site belongs to a different user.\n" +
(username ? "You are currently logged in as " + username + ".\n" : "") +
"\nEither have the site owner use 'meteor authorized --add' to add you\n" +
"as an authorized developer for the site, or switch to an authorized\n" +
@@ -313,12 +316,27 @@ var bundleAndDeploy = function (options) {
if (! site)
return 1;
// We should give a username/password prompt if the user was logged in
// but the credentials are expired, unless the user is logged in but
// doesn't have a username (in which case they should hit the email
// prompt -- a user without a username shouldn't be given a username
// prompt). There's an edge case where things happen in the following
// order: user creates account, user sets username, credential expires
// or is revoked, user comes back to deploy again. In that case,
// they'll get an email prompt instead of a username prompt because
// the command-line tool didn't have time to learn about their
// username before the credential was expired.
auth.pollForRegistrationCompletion({
noLogout: true
});
var promptIfAuthFails = (auth.loggedInUsername() !== null);
// Check auth up front, rather than after the (potentially lengthy)
// bundling process.
var preflight = authedRpc({
site: site,
preflight: true,
promptIfAuthFails: true
promptIfAuthFails: promptIfAuthFails
});
if (preflight.errorMessage) {
@@ -341,7 +359,7 @@ var bundleAndDeploy = function (options) {
var buildDir = path.join(options.appDir, '.meteor', 'local', 'build_tar');
var bundlePath = path.join(buildDir, 'bundle');
process.stdout.write('Deploying to ' + site + '. Bundling...\n');
process.stdout.write('Deploying to ' + site + '. Bundling...\n');
var settings = null;
var messages = buildmessage.capture({
@@ -487,6 +505,7 @@ var checkAuthThenSendRpc = function (site, operation, what) {
return null;
}
} else { // User is logged in but not authorized for this app
process.stderr.write("\n");
printUnauthorizedMessage();
return null;
}
@@ -539,6 +558,7 @@ var logs = function (site) {
return 1;
} else {
process.stdout.write(result.message);
auth.maybePrintRegistrationLink({ leadingNewline: true });
return 0;
}
};
@@ -659,11 +679,12 @@ var claim = function (site) {
});
if (result.errorMessage) {
if (! auth.currentUsername() &&
auth.pollForRegistrationCompletion();
if (! auth.loggedInUsername() &&
auth.registrationUrl()) {
process.stderr.write(
"\nBefore you can claim existing sites, you need to set a password on\n" +
"your Meteor developer account. You can do that here in under a minute:\n\n" +
"You need to set a password on your Meteor developer account before\n" +
"you can claim sites. You can do that here in under a minute:\n\n" +
auth.registrationUrl() + "\n\n");
} else {
process.stderr.write("Couldn't claim site: " +

View File

@@ -218,6 +218,32 @@ site into your account. If you had set a password on the site you will
be prompted for it one last time.
>>> login
Log in to your Meteor developer account
Usage: meteor login [--email]
Prompts for your username and password and logs you in to your Meteor
developer account. Pass --email to log in by email address rather than
by username.
>>> logout
Log out of your Meteor developer account
Usage: meteor logout
Log out of your Meteor developer account.
>>> whoami
Prints the username of your Meteor developer account
Usage: meteor whoami
Prints the username of the currently logged-in Meteor developer.
See 'meteor login' to log into or 'meteor logout' to log out of your
Meteor developer account.
>>> test-packages
Test one or more packages
Usage: meteor test-packages [--release <release>] [options] [package...]
@@ -291,30 +317,31 @@ Prompts for your username and password and logs you in to your
Meteor account. Pass --email to log in by email address instead of
username. Pass --galaxy to specify a galaxy to log in to.
Builds the provided directory as a package, then loads the package and
calls the main() function inside the package. The function will receive
any remaining arguments. The exit status will be the return value of
main() (which is called inside a fiber).
>>> logout
Log out of your Meteor account
Usage: meteor logout
The arguments will be parsed by Meteor's option parser, which means that
--release will be effective (but not passed to the command), and that it will be
an error to pass any unknown options. If you want to pass options to your tool,
place them after a '--' argument (which turns off option parsing for the rest of
the arguments).
Log out of your Meteor account.
>>> whoami
Prints the username of your Meteor developer account
Usage: meteor whoami
Prints the username of the currently logged-in Meteor developer.
See 'meteor login' to log into or 'meteor logout' to log out of of your
Meteor account.
This command is for temporary, internal use, until we have a more mature
system for building standalone command-line programs with Meteor.
>>> self-test
Run tests of the 'meteor' tool.
Usage: meteor self-test [--changed] [--slow] [--force-online] [--history n]
Usage: meteor self-test [pattern] [--changed] [--slow]
[--force-online] [--history n]
Runs internal tests. Exits with status 0 on success.
If 'pattern' is provided, it should be a regular expression. Only
tests that match the regular expression will be run.
Pass --changed to run only tests that have changed since they last
passed. This uses a really rough heuristic: A test has changed iff
there has been any change to the file in the 'selftests' subdirectory
@@ -361,4 +388,3 @@ Grant a permission on an official service
Usage: meteor admin grant [XXX]
Not yet implemented

View File

@@ -477,6 +477,21 @@ _.extend(Sandbox.prototype, {
unlink: function (filename) {
var self = this;
fs.unlinkSync(path.join(self.cwd, filename));
},
// Return the current contents of .meteorsession in the sandbox.
readSessionFile: function () {
var self = this;
return fs.readFileSync(path.join(self.root, '.meteorsession'), 'utf8');
},
// Overwrite .meteorsession in the sandbox with 'contents'. You
// could use this in conjunction with readSessionFile to save and
// restore authentication states.
writeSessionFile: function (contents) {
var self = this;
return fs.writeFileSync(path.join(self.root, '.meteorsession'),
contents, 'utf8');
}
});
@@ -976,7 +991,7 @@ var tagDescriptions = {
// these last two are not actually test tags; they reflect the use of
// --changed and --tests
unchanged: 'unchanged since last pass',
misnamed: "don't match --tests argument"
'non-matching': "don't match specified pattern"
};
// options: onlyChanged, offline, includeSlowTests, historyLines, testRegexp
@@ -1015,7 +1030,7 @@ var runTests = function (options) {
tests = _.filter(tests, function (test) {
return options.testRegexp.test(test.name);
});
skipCounts.misnamed = lengthBeforeTestRegexp - tests.length;
skipCounts['non-matching'] = lengthBeforeTestRegexp - tests.length;
}
if (options.onlyChanged) {

View File

@@ -141,7 +141,7 @@ selftest.define('claim - no username', ['net', 'slow'], function () {
run.matchErr('Password:');
run.write('test\n');
run.waitSecs(commandTimeoutSecs);
run.matchErr('you need to set a password');
run.matchErr('You need to set a password');
run.matchErr(testUtils.registrationUrlRegexp);
run.expectExit(1);
// After we set a username, we should be able to claim sites.

View File

@@ -3,9 +3,63 @@ var selftest = require('../selftest.js');
var testUtils = require('../test-utils.js');
var files = require('../files.js');
var Sandbox = selftest.Sandbox;
var httpHelpers = require('../http-helpers.js');
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
selftest.define('deploy - expired credentials', ['net', 'slow'], function () {
var s = new Sandbox;
// Create an account and then expire the login token before setting a
// username. On the next deploy, we should get an email prompt
// followed by a registration email, not a username prompt.
var email = testUtils.randomUserEmail();
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
var sessionFile = s.readSessionFile();
testUtils.logout(s);
s.writeSessionFile(sessionFile);
var run = s.run('deploy', appName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Expired credential');
run.expectExit(1);
// Complete registration so that we can clean up our app.
var username = testUtils.randomString(10);
testUtils.registerWithToken(token, username,
'testtest', email);
testUtils.login(s, username, 'testtest');
testUtils.cleanUpApp(s, appName);
testUtils.logout(s);
// Create an account, set a username, expire the login token, and
// deploy again. We should get a username/password prompt.
email = testUtils.randomUserEmail();
appName = testUtils.randomAppName();
username = testUtils.randomString(10);
token = testUtils.deployWithNewEmail(s, email, appName);
testUtils.registerWithToken(token, username,
'testtest', email);
run = s.run('whoami');
run.waitSecs(commandTimeoutSecs);
run.read(username + '\n');
run.expectExit(0);
sessionFile = s.readSessionFile();
testUtils.logout(s);
s.writeSessionFile(sessionFile);
run = s.run('deploy', appName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Username:');
run.write(username + '\n');
run.matchErr('Password:');
run.write('testtest' + '\n');
run.waitSecs(90);
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
});
selftest.define('deploy - bad arguments', [], function () {
var s = new Sandbox;
@@ -202,7 +256,7 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(commandTimeoutSecs);
run.matchErr('already in use');
run.matchErr('pick a password');
run.matchErr('An email has been sent to you with the link');
run.stop();
});

View File

@@ -14,8 +14,42 @@ selftest.define("login", ['net'], function () {
// Username and password prompts happen on stderr so that scripts can
// run commands that do login interactively and still save the command
// output with the login prompts appearing in it.
//
// Do this twice to confirm that the login command prints a prompt
// even if you are already logged in.
for (var i = 0; i < 2; i++) {
run = s.run("login");
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("testtest\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged in as test.");
run.expectExit(0);
}
// Leaving username blank, or getting the password wrong, doesn't
// reprompt. It also doesn't log you out.
run = s.run("login");
run.matchErr("Username:");
run.write("\n");
run.matchErr("Password:");
run.write("whatever\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("failed");
run.expectExit(1);
run = s.run("login");
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("whatever\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("failed");
run.expectExit(1);
run = s.run('login');
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("testtest\n");

View File

@@ -71,8 +71,23 @@ var logsOrMongoForApp = function (sandbox, command, appName, options) {
} else {
// If we are not logged in and this is not a legacy app, then we
// expect a login prompt.
//
// (If testReprompt is true, try getting reprompted as a result
// of entering no username or a bad password.)
if (options.testReprompt) {
run.matchErr('Username: ');
run.write("\n");
run.matchErr("Username:");
run.write(" \n");
}
run.matchErr('Username: ');
run.write((options.username || 'test') + '\n');
if (options.testReprompt) {
run.matchErr("Password:");
run.write("wrongpassword\n");
run.waitSecs(15);
run.matchErr("failed");
}
run.matchErr('Password: ');
run.write((options.password || 'testtest') + '\n');
run.waitSecs(commandTimeoutSecs);
@@ -137,7 +152,8 @@ _.each([false, true], function (loggedIn) {
logsOrMongoForApp(s, command,
'app-for-selftest-not-test-owned', {
loggedIn: loggedIn,
authorized: false
authorized: false,
testReprompt: true
});
if (! loggedIn) {

View File

@@ -117,8 +117,13 @@ selftest.define('deferred registration - email registration token', ['net', 'slo
testUtils.registerWithToken(token, username, 'testtest', email);
// Success! We should be able to log out and log back in with our new
// password.
// Success! 'meteor whoami' should now know who we are.
run = s.run('whoami');
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.read(username + '\n');
run.expectExit(0);
// We should be able to log out and log back in with our new password.
testUtils.logout(s);
testUtils.login(s, username, 'testtest');
@@ -133,6 +138,52 @@ selftest.define('deferred registration - email registration token', ['net', 'slo
// XXX Test that registration URLs get printed when they should
});
selftest.define('deferred registration revocation', ['net'], function () {
// Test that if we are logged in as a passwordless user, and our
// credential gets revoked, and we do something like 'meteor whoami'
// that polls to see if registration is complete, then we handle it
// gracefully.
var s = new Sandbox;
s.createApp('deployapp', 'empty');
s.cd('deployapp');
// Create a new deferred registration account. (Don't bother to wait
// for the deploy to go through.)
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var run = s.run('deploy', appName);
run.waitSecs(5);
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(90);
run.match('Deploying');
run.stop();
// 'whoami' says that we don't have a password
run = s.run('whoami');
run.waitSecs(15);
run.matchErr('/setPassword?');
run.expectExit(1);
// Revoke the credential without updating .meteorsession.
var sessionState = s.readSessionFile();
run = s.run('logout');
run.waitSecs(15);
run.readErr("Logged out.\n");
run.expectEnd();
run.expectExit(0);
s.writeSessionFile(sessionState);
// 'whoami' now says that we're not logged in. No errors are printed.
run = s.run('whoami');
run.waitSecs(15);
run.readErr("Not logged in. 'meteor login' to log in.\n");
run.expectEnd();
run.expectExit(1);
});
selftest.define(
'deferred registration - api registration token',
['net', 'slow'],
@@ -185,8 +236,8 @@ selftest.define(
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.matchErr('already in use');
run.matchErr('come back here to deploy your app');
run.matchErr('pick a password');
run.matchErr('Waiting for you to register on the web...');
var registrationEmail = waitForEmail(
email,
@@ -203,8 +254,8 @@ selftest.define(
testUtils.registerWithToken(token[1], username, 'testtest', email);
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.matchErr('log in with your new password');
run.matchErr('Password:');
run.matchErr('Username: ' + username + '\n');
run.matchErr('Password: ');
run.write('testtest\n');
run.waitSecs(90);
run.match('Now serving at');