diff --git a/History.md b/History.md index 9c67443918..62936025db 100644 --- a/History.md +++ b/History.md @@ -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 diff --git a/packages/oauth/oauth_client.js b/packages/oauth/oauth_client.js index c038332de6..dad37006a3 100644 --- a/packages/oauth/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -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( diff --git a/scripts/admin/publish-release/packages/awssum b/scripts/admin/publish-release/packages/awssum index 69dc3c7afb..0221218663 160000 --- a/scripts/admin/publish-release/packages/awssum +++ b/scripts/admin/publish-release/packages/awssum @@ -1 +1 @@ -Subproject commit 69dc3c7afb455cb487b6fbe551478384dea0cb0b +Subproject commit 0221218663b12b47f61cd9745da9c2be3f247bdb diff --git a/tools/auth.js b/tools/auth.js index b679086441..3378fd7c5c 100644 --- a/tools/auth.js +++ b/tools/auth.js @@ -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) { diff --git a/tools/commands.js b/tools/commands.js index 71d068a032..3a003dd6c5 100644 --- a/tools/commands.js +++ b/tools/commands.js @@ -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; } } diff --git a/tools/deploy-galaxy.js b/tools/deploy-galaxy.js index 46377b5559..452ec7d033 100644 --- a/tools/deploy-galaxy.js +++ b/tools/deploy-galaxy.js @@ -150,7 +150,7 @@ _.extend(ServiceConnection.prototype, { } }); - self.subscribe.apply(self, args); + self.connection.subscribe.apply(self.connection, args); return fut.wait(); }, diff --git a/tools/deploy.js b/tools/deploy.js index 4524014edb..84e82db94d 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -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: " + diff --git a/tools/help.txt b/tools/help.txt index 068e489cba..6ca85098a0 100644 --- a/tools/help.txt +++ b/tools/help.txt @@ -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 ] [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 - diff --git a/tools/selftest.js b/tools/selftest.js index bfabb4db87..8be27aef85 100644 --- a/tools/selftest.js +++ b/tools/selftest.js @@ -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) { diff --git a/tools/tests/claim.js b/tools/tests/claim.js index 9d2d3a5680..cde0984170 100644 --- a/tools/tests/claim.js +++ b/tools/tests/claim.js @@ -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. diff --git a/tools/tests/deploy-auth.js b/tools/tests/deploy-auth.js index 9fae26a238..f1c193aa60 100644 --- a/tools/tests/deploy-auth.js +++ b/tools/tests/deploy-auth.js @@ -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(); }); diff --git a/tools/tests/login.js b/tools/tests/login.js index 02a3763f66..25b3dc0ecb 100644 --- a/tools/tests/login.js +++ b/tools/tests/login.js @@ -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"); diff --git a/tools/tests/logs-mongo-auth.js b/tools/tests/logs-mongo-auth.js index 3a8de6fa60..8ab2358914 100644 --- a/tools/tests/logs-mongo-auth.js +++ b/tools/tests/logs-mongo-auth.js @@ -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) { diff --git a/tools/tests/registration.js b/tools/tests/registration.js index b7e93c26c4..ee2eac71ab 100644 --- a/tools/tests/registration.js +++ b/tools/tests/registration.js @@ -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');