diff --git a/.mailmap b/.mailmap index 40bbb70898..7132327b50 100644 --- a/.mailmap +++ b/.mailmap @@ -15,11 +15,13 @@ GITHUB: codeinthehole GITHUB: dandv GITHUB: DenisGorbachev GITHUB: emgee3 +GITHUB: EOT GITHUB: FooBarWidget GITHUB: jacott GITHUB: Maxhodges GITHUB: meawoppl GITHUB: mitar +GITHUB: mitar GITHUB: mizzao GITHUB: mquandalle GITHUB: nathan-muir @@ -28,6 +30,14 @@ GITHUB: ryw GITHUB: rzymek GITHUB: sdarnell GITHUB: timhaines +GITHUB: jfhamlin +GITHUB: marcandre +GITHUB: michaelbishop +GITHUB: OyoKooN +GITHUB: paulswartz +GITHUB: rdickert +GITHUB: icellan +GITHUB: yeputons METEOR: avital METEOR: debergalis @@ -40,3 +50,5 @@ METEOR: karayu METEOR: n1mmy METEOR: sixolet METEOR: Slava +METEOR: stubailo +METEOR: ekatek diff --git a/History.md b/History.md index a3e94ac32d..88a55862a1 100644 --- a/History.md +++ b/History.md @@ -1,85 +1,57 @@ ## v.NEXT -* Meteor developer accounts - - `accounts-meteor-developer` package for OAuth support - - managing deployed apps with developer accounts instead of site - passwords. - - New commands: 'meteor authorized', 'meteor claim', 'meteor logout', - 'meteor whoami' +## v0.7.1.1 -* oplog improvements - - support all operators except $where and $near. still not used for - limit and skip - - more optimizations to avoid needless data fetches from MongoDB - - fix "Cannot call method 'has' of null" error #1767 +* Integrate with Meteor developer accounts, a new way of managing your + meteor.com deployed sites. When you use `meteor deploy`, you will be + prompted to create a developer account. + - Once you've created a developer account, you can log in and out + from the command line with `meteor login` and `meteor logout`. + - You can claim legacy sites with `meteor claim`. This command will + prompt you for your site password if you are claiming a + password-protected site; after claiming it, you will not need to + enter the site password again. + - You can add or remove authorized users, and view the list of + authorized users, for a site with `meteor authorized`. + - You can view your current username with `meteor whoami`. + - This release also includes the `accounts-meteor-developer` package + for building Meteor apps that allow users to log in with their own + developer accounts. -* Minimongo improvements - - support $comment - - support 'obj' name in $where - - $regexp matches actual regexps properly - - better support for $nin, $ne, $not - - support using { $in: [/foo/, /bar/] }. #1707 - - support {$exists: false} - - better type-checking for selectors - - support {x: {$elemMatch: {$gt: 5}}} - - match mongo's behavior better when there are arrays in the document - - support $near with sort - - implement updates with { $set: { 'a.$.b': 5 } } - - {$type: 4} queries - - optimize `remove({})` when observers are paused - - make update-by-id constant time +* Improve the oplog tailing implementation for getting real-time database + updates from MongoDB. + - Add support for all operators except `$where` and `$near`. Limit and + skip are not supported yet. + - Add optimizations to avoid needless data fetches from MongoDB. + - Fix an error ("Cannot call method 'has' of null") in an oplog + callback. #1767 -* Add `clientAddress` and `httpHeaders` to `this.connection` in method - calls and publish functions. +* Add and improve support for minimongo operators. + - Support `$comment`. + - Support `obj` name in `$where`. + - `$regexp` matches actual regexps properly. + - Improve support for `$nin`, `$ne`, `$not`. + - Support using `{ $in: [/foo/, /bar/] }`. #1707 + - Support `{$exists: false}`. + - Improve type-checking for selectors. + - Support `{x: {$elemMatch: {$gt: 5}}}`. + - Match Mongo's behavior better when there are arrays in the document. + - Support `$near` with sort. + - Implement updates with `{ $set: { 'a.$.b': 5 } }`. + - Support `{$type: 4}` queries. + - Optimize `remove({})` when observers are paused. + - Make update-by-id constant time. + - Allow `{$set: {'x._id': 1}}`. #1794 -* Hash login tokens before storing them in the database. Legacy unhashed - tokens are upgraded to hashed tokens in the database as they are used - in logins. - -* Cursors with a field specifier containing `{_id: 0}` can no longer be used - with `observeChanges` or `observe`. This includes the implicit calls to these - functions that are done when returning a cursor from a publish function or - using `{{#each}}`. - -* Transform functions must return objects and may not change the `_id` field - (though they may leave it out) - -* XXX sourcemaps support for stylesheets, including less sourcemaps -* XXX css linting (breaks on errors) -* XXX css preprocessing to concatenate files correctly (pulls @imports to the - beginning) -* XXX supports `.import.less` and `.import.styl` to prevent Meteor processing - stylesheets. `.lessimport` is deprecated - -* Patch Underscore to not treat plain objects (`x.constructor === Object`) - with numeric `length` fields as arrays. Among other things, this allows you - to use documents with numeric `length` fields with Mongo. #594 #1737 - -* Fix races when calling login and/or logoutOtherClients from multiple - tabs. #1616 - -* Upgrade `jquery-waypoints` package from 1.1.7 to 2.0.4. (Contains - backward-incompatible changes). - -* Add `frame-src` to `browser-policy-content` and account for - cross-browser CSP disparities. - -* Upgrade CoffeeScript from 1.6.3 to 1.7.1. - -* Make sure that `api.add_files('foo.coffee', {bare: true})` works when - adding CoffeeScript files. #1668 - -* `force-ssl`: don't require SSL during `meteor run` in IPv6 - environments. #1751 - -* Upgraded dependencies: - - node from 0.10.22 to 0.10.25 (removed workaround from 0.7.0 -- now - support 0.10.25+) - - Upgrade jQuery from 1.8.2 to 1.11.0 - XXX see http://jquery.com/upgrade-guide/1.9/ for incompatibilities (maybe - goes in notices?) - - source-map from 0.3.30 to 0.3.32 #1782 - - websocket-driver from 0.3.1 to 0.3.2 +* Upgraded dependencies + - node: 0.10.25 (from 0.10.22). The workaround for specific Node + versions from 0.7.0 is now removed; 0.10.25+ is supported. + - jquery: 1.11.0 (from 1.8.2). See + http://jquery.com/upgrade-guide/1.9/ for upgrade instructions. + - jquery-waypoints: 2.0.4 (from 1.1.7). Contains + backwards-incompatible changes. + - source-map: 0.3.2 (from 0.3.30) #1782 + - websocket-driver: 0.3.2 (from 0.3.1) - http-proxy: 1.0.2 (from a pre-release fork of 1.0) - semver: 2.2.1 (from 2.1.0) - request: 2.33.0 (from 2.27.0) @@ -90,73 +62,138 @@ - source-map-support: 0.2.5 (from 0.2.3) - mongo: 2.4.9 (from 2.4.8) - openssl in mongo: 1.0.1f (from 1.0.1e) - - kexec from 0.1.1 to 0.2.0 - - drop shell-quote from dev bundle - - XXX upgraded `less` from 1.3.3 to 1.6.1 - - XXX upgraded `stylus` from 0.37.0 to 0.42.2 and `nib` from `1.0.0` to `1.0.2` + - kexec: 0.2.0 (from 0.1.1) + - less: 1.6.1 (from 1.3.3) + - stylus: 0.42.2 (from 0.37.0) + - nib: 1.0.2 (from 1.0.0) + - coffeescript: 1.7.1 (from 1.6.3) -* Types added with `EJSON.addType` now have default `clone` and `equals` - implementations. #1745 +* CSS preprocessing and sourcemaps: + - Add sourcemap support for CSS stylesheet preprocessors. Use + sourcemaps for stylesheets compiled with LESS. + - Improve CSS minification to deal with `@import` statements correctly. + - Lint CSS files for invalid `@` directives. + - Change the recommended suffix for imported LESS files from + `.lessimport` to `.import.less`. Add `.import.styl` to allow + `stylus` imports. `.lessimport` continues to work but is deprecated. -* Allow cursors on named local collections to be returned from arrays in publish - functions. #1820 +* Add `clientAddress` and `httpHeaders` to `this.connection` in method + calls and publish functions. -* Don't lose permissions (eg, executable bit) on npm files. #1808 - -* Detect if CSS failed to load and refresh. - -* Don't crash with an empty programs/foo directory or one without a - package.js. - -* Do a better job of handling shrinkwrap files when a npm module depends - on something that isn't a semver. #1684 - -* Fix failures updating npm dependencies when a node_modules directory exists - above the project directory. #1761 - -* In email package, print a message in dev mode when email is not sent. #1196 - -* Meteor accounts logins (or anything else using the `localstorage` package) no - longer persist in IE7. - -* Fix `accounts-password` login with private-browsing Safari (and - generally, the use of the `localstorage` package). #1291 - -* New `retry` package. - -* Pass through `update` and `remove` return values for validated - operations. #1759 +* Hash login tokens before storing them in the database. Legacy unhashed + tokens are upgraded to hashed tokens in the database as they are used + in login requests. * Change default accounts-ui styling and add more CSS classes. -* Don't leak sockets on error in dev-mode proxy. +* Refactor command-line tool. Add test harness and better tests. Run + `meteor self-test --help` for info on running the tools test suite. -* XXX Make springboard actually exec +* Speed up application re-build in development mode by re-using file + hash computation between file change watching code and application + build code.. -* Speed up build by re-using file hashes. +* Fix issues with documents containing a key named `length` with a + numeric value. Underscore treated these as arrays instead of objects, + leading to exceptions when . Patch Underscore to not treat plain + objects (`x.constructor === Object`) with numeric `length` fields as + arrays. #594 #1737 -* Include oauth_verifier as a header rather than a parameter in - `oauth1` package. #1825 +* Deprecate `Accounts.loginServiceConfiguration` in favor of + `ServiceConfiguration.configurations`, exported by the + `service-configuration` package. `Accounts.loginServiceConfiguration` + is maintained for backwards-compatibility, but it is defined in a + `Meteor.startup` block and so cannot be used from top-level code. -* Refactor command-line tool. Add test harness and better tests. +* Cursors with a field specifier containing `{_id: 0}` can no longer be + used with `observeChanges` or `observe`. This includes the implicit + calls to these functions that are done when returning a cursor from a + publish function or using `{{#each}}`. -* Add `Accounts.connection` for using Meteor accounts packages with a - non-default DDP connection. +* Transform functions must return objects and may not change the `_id` + field, though they may leave it out. -* Fix order of setting login token and setting user id on a connection. +* Remove broken IE7 support from the `localstorage` package. Meteor + accounts logins no longer persist in IE7. + +* Fix the `localstorage` package when used with Safari in private + browsing mode. This fixes a problem with login token storage and + account login. #1291 + +* Types added with `EJSON.addType` now have default `clone` and `equals` + implementations. Users may still specify `clone` or `equals` functions + to override the default behavior. #1745 + +* Add `frame-src` to `browser-policy-content` and account for + cross-browser CSP disparities. + +* Deprecate `Oauth.initiateLogin` in favor of `Oauth.showPopup`. + +* Add `WebApp.rawConnectHandlers` for adding connect handlers that run + before any other Meteor handlers, except `connect.compress()`. Raw + connect handlers see the URL's full path (even if ROOT_URL contains a + non-empty path) and they run before static assets are served. + +* Add `Accounts.connection` to allow using Meteor accounts packages with + a non-default DDP connection. + +* Detect and reload if minified CSS files fail to load at startup. This + prevents the application from running unstyled if the page load occurs + while the server is switching versions. + +* Allow Npm.depends to specify any http or https URL containing a full + 40-hex-digit SHA. #1686 + +* Add `retry` package for connection retry with exponential backoff. + +* Pass `update` and `remove` return values correctly when using + collections validated with `allow` and `deny` rules. #1759 * If you're using Deps on the server, computations and invalidation - functions are not allowed to yield. + functions are not allowed to yield. Throw an error instead of behaving + unpredictably. + +* Fix namespacing in coffeescript files added to a package with the + `bare: true` option. #1668 + +* Fix races when calling login and/or logoutOtherClients from multiple + tabs. #1616 + +* Include oauth_verifier as a header rather than a parameter in + the `oauth1` package. #1825 + +* Fix `force-ssl` to allow local development with `meteor run` in IPv6 + environments. #1751` + +* Allow cursors on named local collections to be returned from a publish + function in an array. #1820 + +* Fix build failure caused by a directory in `programs/` without a + package.js file. + +* Do a better job of handling shrinkwrap files when an npm module + depends on something that isn't a semver. #1684 + +* Fix failures updating npm dependencies when a node_modules directory + exists above the project directory. #1761 + +* Preserve permissions (eg, executable bit) on npm files. #1808 * SockJS tweak to support relative base URLs. -* Oauth.initiateLogin is deprecated in favor of Oauth.showPopup. +* Don't leak sockets on error in dev-mode proxy. -* User-supplied connect handlers now see the URL's full path, even if - ROOT_URL contains a non-empty path. +* Clone arguments to `added` and `changed` methods in publish + functions. This allows callers to reuse objects and prevents already + published data from changing after the fact. #1750 -* Don't cache direct references to the fields arguments to the subscription - `added` and `changed` methods. #1750 +* Ensure springboarding to a different meteor tools version always uses + `exec` to run the old version. This simplifies process management for + wrapper scripts. + +Patches contributed by GitHub users DenisGorbachev, EOT, OyoKooN, awwx, +dandv, icellan, jfhamlin, marcandre, michaelbishop, mitar, mizzao, +mquandalle, paulswartz, rdickert, rzymek, timhaines, and yeputons. ## v0.7.0.1 diff --git a/LICENSE.txt b/LICENSE.txt index ad05f62cf7..082c5e26ce 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -71,6 +71,13 @@ handlebars: https://github.com/wycats/handlebars.js/ Copyright (C) 2011 by Yehuda Katz +---------- +clean-css: https://github.com/GoalSmashers/clean-css +---------- + +Copyright (c) 2011 GoalSmashers.com + + ---------- progress: https://github.com/visionmedia/node-progress qs: https://github.com/visionmedia/node-querystring @@ -285,6 +292,7 @@ deep-equal: https://github.com/substack/node-deep-equal editor: https://github.com/substack/node-editor minimist: https://github.com/substack/minimist quotemeta: https://github.com/substack/quotemeta +text-table: https://github.com/substack/text-table ---------- Copyright 2010, 2011, 2012, 2013 James Halliday (mail@substack.net) @@ -587,7 +595,7 @@ Copyright 2009 Google Inc. ---------- request: https://github.com/mikeal/request -aws-sign: https://github.com/mikeal/aws-sign +aws-sign2: https://github.com/mikeal/aws-sign cookie-jar: https://github.com/mikeal/cookie-jar forever-agent: https://github.com/mikeal/forever-agent oauth-sign: https://github.com/mikeal/oauth-sign @@ -646,6 +654,35 @@ Diff Match and Patch: http://code.google.com/p/google-diff-match-patch/ Copyright 2006 Google Inc. +---------- +ansicolors: https://github.com/thlorenz/ansicolors +ansistyles: https://github.com/thlorenz/ansistyles +---------- + +Copyright 2013 Thorsten Lorenz. + + +---------- +columnify: https://github.com/timoxley/columnify +---------- + +Copyright Tim Oxley + + +---------- +eventemitter3: https://github.com/3rd-Eden/EventEmitter3 +---------- + +Copyright Arnout Kazemier + + +---------- +punycode: https://github.com/bestiejs/punycode.js +---------- + +Copyright Mathias Bynens + + ============ BSD Licenses @@ -1346,6 +1383,25 @@ required for its use. Other ===== +---------- +heapdump: https://github.com/bnoordhuis/node-heapdump +---------- + +Copyright (c) 2012, Ben Noordhuis + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------- mimelib-noiconv: https://github.com/andris9/mimelib mailcomposer: https://github.com/andris9/mailcomposer @@ -1396,6 +1452,7 @@ By Isaac Z. Schlueter (http://blog.izs.me/) ---------- opener: https://github.com/domenic/opener +path-is-inside: https://github.com/domenic/path-is-inside ---------- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE @@ -1444,6 +1501,91 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to +---------- +tough-cookie: https://github.com/goinstant/tough-cookie +---------- + +Copyright GoInstant, Inc. and other contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +The following exceptions apply: + +=== + +`pubSufTest()` of generate-pubsuffix.js is in the public domain. + + // Any copyright is dedicated to the Public Domain. + // http://creativecommons.org/publicdomain/zero/1.0/ + +=== + +`public-suffix.txt` was obtained from + +via . + +That file contains the usual Mozilla triple-license, for which this project uses it +under the terms of the MPL 1.1: + + // ***** BEGIN LICENSE BLOCK ***** + // Version: MPL 1.1/GPL 2.0/LGPL 2.1 + // + // The contents of this file are subject to the Mozilla Public License Version + // 1.1 (the "License"); you may not use this file except in compliance with + // the License. You may obtain a copy of the License at + // http://www.mozilla.org/MPL/ + // + // Software distributed under the License is distributed on an "AS IS" basis, + // WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + // for the specific language governing rights and limitations under the + // License. + // + // The Original Code is the Public Suffix List. + // + // The Initial Developer of the Original Code is + // Jo Hermans . + // Portions created by the Initial Developer are Copyright (C) 2007 + // the Initial Developer. All Rights Reserved. + // + // Contributor(s): + // Ruben Arakelyan + // Gervase Markham + // Pamela Greene + // David Triendl + // Jothan Frakes + // The kind representatives of many TLD registries + // + // Alternatively, the contents of this file may be used under the terms of + // either the GNU General Public License Version 2 or later (the "GPL"), or + // the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + // in which case the provisions of the GPL or the LGPL are applicable instead + // of those above. If you wish to allow use of your version of this file only + // under the terms of either the GPL or the LGPL, and not to allow others to + // use your version of this file under the terms of the MPL, indicate your + // decision by deleting the provisions above and replace them with the notice + // and other provisions required by the GPL or the LGPL. If you do not delete + // the provisions above, a recipient may use your version of this file under + // the terms of any one of the MPL, the GPL or the LGPL. + // + // ***** END LICENSE BLOCK ***** + + + ---------- MongoDB: http://www.mongodb.org/ ---------- diff --git a/docs/.meteor/release b/docs/.meteor/release index 400877328a..7fce57eeba 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -sso-1 +0.7.1.1 diff --git a/docs/client/api.html b/docs/client/api.html index cbd73d35a8..284f73fa90 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1629,6 +1629,16 @@ This function is provided by the `accounts-password` package. See the {{> api_box loginWithExternalService}} +Available functions are: + +* `Meteor.loginWithMeteorDeveloperAccount` +* `Meteor.loginWithFacebook` +* `Meteor.loginWithGithub` +* `Meteor.loginWithGoogle` +* `Meteor.loginWithMeetup` +* `Meteor.loginWithTwitter` +* `Meteor.loginWithWeibo` + These functions initiate the login process with an external service (eg: Facebook, Google, etc), using OAuth. When called they open a new pop-up window that loads the provider's login page. Once the user has logged in @@ -1659,14 +1669,15 @@ External login services typically require registering and configuring your application before use. The easiest way to do this is with the [`accounts-ui` package](#accountsui) which presents a step-by-step guide to configuring each service. However, the data can be also be entered -manually in the `Accounts.loginServiceConfiguration` collection. For -example: +manually in the `ServiceConfiguration.configurations` collection, which +is exported by the `service-configuration` package. For example, after +running `meteor add service-configuration` in your app: // first, remove configuration entry in case service is already configured - Accounts.loginServiceConfiguration.remove({ + ServiceConfiguration.configurations.remove({ service: "weibo" }); - Accounts.loginServiceConfiguration.insert({ + ServiceConfiguration.configurations.insert({ service: "weibo", clientId: "1292962797", secret: "75a730b58f5691de5522789070c319bc" diff --git a/docs/client/docs.js b/docs/client/docs.js index e29ca72685..f5b45aa215 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -201,13 +201,7 @@ var toc = [ "Meteor.logout", "Meteor.logoutOtherClients", "Meteor.loginWithPassword", - {name: "Meteor.loginWithMeteorDeveloperAccount", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithFacebook", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithGithub", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithGoogle", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithMeetup", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithTwitter", id: "meteor_loginwithexternalservice"}, - {name: "Meteor.loginWithWeibo", id: "meteor_loginwithexternalservice"}, + {name: "Meteor.loginWith", id: "meteor_loginwithexternalservice"}, {type: "spacer"}, {name: "{{currentUser}}", id: "template_currentuser"}, diff --git a/docs/lib/release-override.js b/docs/lib/release-override.js index 60276835d0..646ed35154 100644 --- a/docs/lib/release-override.js +++ b/docs/lib/release-override.js @@ -1,5 +1,5 @@ // While galaxy apps are on their own special meteor releases, override // Meteor.release here. if (Meteor.isClient) { - Meteor.release = Meteor.release ? "0.7.0.1" : undefined; + Meteor.release = Meteor.release ? "0.7.1.1" : undefined; } diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index b6e63167d2..7fce57eeba 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.7.0.1 +0.7.1.1 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index b6e63167d2..7fce57eeba 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.7.0.1 +0.7.1.1 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index b6e63167d2..7fce57eeba 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.7.0.1 +0.7.1.1 diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index b6e63167d2..7fce57eeba 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.7.0.1 +0.7.1.1 diff --git a/packages/accounts-meteor-developer/package.js b/packages/accounts-meteor-developer/package.js index 10b44302b2..682c11e058 100644 --- a/packages/accounts-meteor-developer/package.js +++ b/packages/accounts-meteor-developer/package.js @@ -1,6 +1,5 @@ Package.describe({ - summary: "Login service for Meteor developer accounts", - internal: true // XXX for now + summary: "Login service for Meteor developer accounts" }); Package.on_use(function (api) { diff --git a/packages/accounts-ui-unstyled/login_buttons_single.js b/packages/accounts-ui-unstyled/login_buttons_single.js index 13946bef02..79d35a4190 100644 --- a/packages/accounts-ui-unstyled/login_buttons_single.js +++ b/packages/accounts-ui-unstyled/login_buttons_single.js @@ -43,7 +43,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () { // XXX we should allow service packages to set their capitalized name return 'GitHub'; else if (this.name === 'meteor-developer') - return 'a Meteor developer account'; + return 'Meteor'; else return capitalize(this.name); }; diff --git a/packages/autopublish/package.js b/packages/autopublish/package.js index 960a18afd8..f7c106482e 100644 --- a/packages/autopublish/package.js +++ b/packages/autopublish/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Automatically publish the entire database to all clients" + summary: "Publish the entire database to all clients" }); // This package is empty; its presence is detected by livedata and diff --git a/packages/cookies/.gitignore b/packages/cookies/.gitignore deleted file mode 100644 index 677a6fc263..0000000000 --- a/packages/cookies/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* diff --git a/packages/cookies/cookies.js b/packages/cookies/cookies.js deleted file mode 100644 index a20443d929..0000000000 --- a/packages/cookies/cookies.js +++ /dev/null @@ -1,59 +0,0 @@ -Cookies = {}; - -// Given the value of a Cookie header, returns a dictionary of cookie -// keys => values. If passed the empty string (or a string that is all -// whitespace), returns {} -// -// cookieString is the value of document.cookies or a Cookie header, -// for example "a=b; c=d". -Cookies.parse = function (cookieString) { - var cookies = {}; - var cookieParts = cookieString.split(/\s*;\s*/); - _.each(cookieParts, function (part) { - var match = part.match(/^([^=]+)=(.*)/); - if (match) - // Browsers are not supposed to send multiple values for the - // same cookie, but if they do, do the easy thing, which is to - // take the last value seen. - cookies[match[1]] = match[2]; - }); - - return cookies; -}; - - -// Given a dictionary of cookie names and values, return a Cookie -// header (as parsed by Cookies.parse). -// -// No attempt is made to sanitize or quote characters in the cookie -// name or value. Behavior varies between browsers, between RFCs, and -// between browsers and RFCs. If you want to play it safe, good advice -// would be to limit cookie names to alphanumerics, dashes, and -// underscores, and limit cookie values to printable ASCII characters -// excluding quote, comma, semicolon, backspace, and whitespace. -Cookies.stringify = function (cookies) { - // RFC6265 says that valid characters in a cookie name are: - // token = - // RFC2616 is HTTP 1.1 and defines 'token' as: - // token = 1* - // separators = "(" | ")" | "<" | ">" | "@" - // | "," | ";" | ":" | "\" | <"> - // | "/" | "[" | "]" | "?" | "=" - // | "{" | "}" | SP | HT - // - // RFC6265 says that valid characters in a cookie value are: - // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) - // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - // ; US-ASCII characters excluding CTLs, - // ; whitespace DQUOTE, comma, semicolon, - // ; and backslash - // - // In practice, browsers (at least Chrome) permit a wider range of - // characters in cookie values (such as, in Chome, at least comma - // and double quote). So for now we're going to not worry about this - // and trust the user to know what characters their targeted - // browsers tolerate. - return _.map(cookies, function (value, key) { - return key + "=" + value; - }).join(";"); -}; diff --git a/packages/cookies/cookies_client.js b/packages/cookies/cookies_client.js deleted file mode 100644 index 595ff11be5..0000000000 --- a/packages/cookies/cookies_client.js +++ /dev/null @@ -1,55 +0,0 @@ -// If the current origin (and path) has a (non-HttpOnly) cookie for -// 'name', return it. Otherwise return null. -Cookies.get = function (name) { - var cookies = Cookies.parse(document.cookie || ''); - return _.has(cookies, name) ? cookies[name] : null; -}; - -// Set a cookie 'name'='value on the current origin. It is the -// caller's responsibility to ensure that 'name' and 'value' contain -// only characters that are legal in cookie names and values. -// -// Options may include: -// -// - path: Path prefix for which the cookie should be sent. If not -// specified, defaults to the current path of document.location. -// -// - domain: Domain for which the cookie should be sent. Default to -// the current domain (more precisely, the host part of the current -// document.location). Use ".mysite.com" to send to mysite.com and -// all of mysite's subdomains. -// -// - maxAge: How long the cookie should live (in seconds). If not -// provided, the cookie will expire at the end of the browser -// session. -// -// - secure: If true, provide this cookie only for secure (https) -// connections. If you call this from a page that was loaded over -// http, the cookie will be set but you won't be able to read it -// back unless the user reloads the page over https. -// -// To delete a cookie, set maxAge to zero, passing the same name, -// domain, and path. -Cookies.set = function (name, value, options) { - options = options || {}; - - var cmd = name + '=' + value; - if (_.has(options, 'path')) - cmd += ";path=" + options.path; - if (_.has(options, 'domain')) - cmd += ";domain=" + options.domain; - if (_.has(options, "maxAge")) { - // Not all browsers support 'max-age', but all support 'expires'. - var when = new Date((new Date).getTime() + options.maxAge * 1000); - cmd += ";expires=" + when.toUTCString(); - } - if (_.has(options, "secure")) - cmd += ";secure"; - - // This does not set document.cookie. It causes the browser to - // behave as if it had received a Set-Cookie header with the value - // 'cmd'. - // - // "This is the worst interface I have ever seen in my life." -- Emily - document.cookie = cmd; -}; diff --git a/packages/cookies/cookies_client_test.js b/packages/cookies/cookies_client_test.js deleted file mode 100644 index fd2f31a51e..0000000000 --- a/packages/cookies/cookies_client_test.js +++ /dev/null @@ -1,14 +0,0 @@ -Tinytest.add("cookies - browser set/get", function (test) { - var name = Random.id(); - - test.equal(Cookies.get(name), null); - Cookies.set(name, "hello"); - test.equal(Cookies.get(name), "hello"); - Cookies.set(name, "hello again"); - test.equal(Cookies.get(name), "hello again"); - Cookies.set(name, "stuff", { maxAge: 0 }); - test.equal(Cookies.get(name), null); - Cookies.set(name, "kitten", { path: "/somewhere-else" }); - test.equal(Cookies.get(name), null); - Cookies.set(name, "kitten", { path: "/somewhere-else", maxAge: 0 }); -}); diff --git a/packages/cookies/cookies_test.js b/packages/cookies/cookies_test.js deleted file mode 100644 index 21a94f66ff..0000000000 --- a/packages/cookies/cookies_test.js +++ /dev/null @@ -1,12 +0,0 @@ -Tinytest.add("cookies - parse and stringify", function (test) { - test.equal(Cookies.parse("a=b; c=d"), {a: "b", c: "d"}); - test.equal(Cookies.parse("a=b;c=d"), {a: "b", c: "d"}); - test.equal(Cookies.parse("a=b;c=d;e=12"), {a: "b", c: "d", e: "12"}); - test.equal(Cookies.parse("a=b=c;d=e=f"), {a: "b=c", d: "e=f"}); - - // results depends on object key order being preserved, but it will - // probably work in all of our test hosts - test.equal(Cookies.stringify({a: "1"}), "a=1"); - test.equal(Cookies.stringify({a: "1", b: "2"}), "a=1;b=2"); - test.equal(Cookies.stringify({a: "a=1", b: "b=2"}), "a=a=1;b=b=2"); -}); diff --git a/packages/cookies/package.js b/packages/cookies/package.js deleted file mode 100644 index fc17b0677c..0000000000 --- a/packages/cookies/package.js +++ /dev/null @@ -1,20 +0,0 @@ -Package.describe({ - summary: "Parsing cookies", - internal: true -}); - -Package.on_use(function (api) { - api.use('underscore', ['client', 'server']); - api.export('Cookies', ['client', 'server']); - api.add_files('cookies.js', ['client', 'server']); - api.add_files('cookies_client.js', ['client']); -}); - -Package.on_test(function (api) { - api.use('cookies', ['client', 'server']); - api.use('tinytest', ['client', 'server']); - api.use('random', ['client']); - - api.add_files('cookies_test.js', ['client', 'server']); - api.add_files('cookies_client_test.js', ['client']); -}); diff --git a/packages/force-ssl/package.js b/packages/force-ssl/package.js index 11eddfc516..ee7780482f 100644 --- a/packages/force-ssl/package.js +++ b/packages/force-ssl/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Require this application to use secure transport (HTTPS)" + summary: "Require this application to use HTTPS" }); Package.on_use(function (api) { diff --git a/packages/jquery-waypoints/package.js b/packages/jquery-waypoints/package.js index db5d91b6c8..80c25aa893 100644 --- a/packages/jquery-waypoints/package.js +++ b/packages/jquery-waypoints/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Execute a function when the user scrolls past an element" + summary: "Run a function when the user scrolls past an element" }); Package.on_use(function (api) { diff --git a/packages/less/less_tests_empty.less b/packages/less/less_tests_empty.less new file mode 100644 index 0000000000..8df92dd07e --- /dev/null +++ b/packages/less/less_tests_empty.less @@ -0,0 +1,2 @@ +#id {} + diff --git a/packages/less/package.js b/packages/less/package.js index 4e242a699a..120f10e7d3 100644 --- a/packages/less/package.js +++ b/packages/less/package.js @@ -14,6 +14,6 @@ Package._transitional_registerBuildPlugin({ Package.on_test(function (api) { api.use(['test-helpers', 'tinytest', 'less']); api.use(['spark']); - api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.import.less'], - 'client'); + api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.import.less', + 'less_tests_empty.less'], 'client'); }); diff --git a/packages/less/plugin/compile-less.js b/packages/less/plugin/compile-less.js index 73f24b5206..fb8265abe7 100644 --- a/packages/less/plugin/compile-less.js +++ b/packages/less/plugin/compile-less.js @@ -42,23 +42,25 @@ Plugin.registerSourceHandler("less", function (compileStep) { return; } - var cssFuture = new Future; + var sourceMap = null; var css = ast.toCSS({ - sourceMap: Boolean(true), - writeSourceMap: function (sourceMap) { - cssFuture.return(sourceMap); + sourceMap: true, + writeSourceMap: function (sm) { + sourceMap = JSON.parse(sm); } }); - var sourceMap = JSON.parse(cssFuture.wait()); - sourceMap.sources = [compileStep.inputPath]; - sourceMap.sourcesContent = [source]; + if (sourceMap) { + sourceMap.sources = [compileStep.inputPath]; + sourceMap.sourcesContent = [source]; + sourceMap = JSON.stringify(sourceMap); + } compileStep.addStylesheet({ path: compileStep.inputPath + ".css", data: css, - sourceMap: JSON.stringify(sourceMap) + sourceMap: sourceMap }); });; diff --git a/packages/meteor-developer/package.js b/packages/meteor-developer/package.js index e076d9049f..39691f01e9 100644 --- a/packages/meteor-developer/package.js +++ b/packages/meteor-developer/package.js @@ -1,5 +1,6 @@ Package.describe({ - summary: "Meteor developer accounts OAuth flow" + summary: "Meteor developer accounts OAuth flow", + internal: true }); Package.on_use(function (api) { diff --git a/packages/preserve-inputs/package.js b/packages/preserve-inputs/package.js index f979a2d5d9..e0c10aa02c 100644 --- a/packages/preserve-inputs/package.js +++ b/packages/preserve-inputs/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Automatically preserve all form fields with a unique id" + summary: "Automatically preserve form fields with a unique id" }); Package.on_use(function (api) { diff --git a/packages/underscore/package.js b/packages/underscore/package.js index 0d0c0459c3..8f572bd5ca 100644 --- a/packages/underscore/package.js +++ b/packages/underscore/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Collection of small helper functions: _.map, _.each, ..." + summary: "Collection of small helpers: _.map, _.each, ..." }); Package.on_use(function (api) { diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 6bf5e22afe..ad3fe4e1cb 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -247,19 +247,13 @@ var runWebAppServer = function () { // webserver var app = connect(); - // Parse the query string into res.query. Used by oauth_server, but it's - // generally pretty handy.. - app.use(connect.query()); - // Auto-compress any json, javascript, or text. app.use(connect.compress()); - // Packages and apps can add handlers to this via - // WebApp.connectHandlers. They are inserted before our default - // handler. If a path prefix is in use, they see the actual - // requested URL before the path prefix has been stripped off. - var packageAndAppHandlers = connect(); - app.use(packageAndAppHandlers); + // Packages and apps can add handlers that run before any other Meteor + // handlers via WebApp.rawConnectHandlers. + var rawConnectHandlers = connect(); + app.use(rawConnectHandlers); // Strip off the path prefix, if it exists. app.use(function (request, response, next) { @@ -284,6 +278,10 @@ var runWebAppServer = function () { } }); + // Parse the query string into res.query. Used by oauth_server, but it's + // generally pretty handy.. + app.use(connect.query()); + var getItemPathname = function (itemUrl) { return decodeURIComponent(url.parse(itemUrl).pathname); }; @@ -415,6 +413,11 @@ var runWebAppServer = function () { .pipe(res); }); + // Packages and apps can add handlers to this via WebApp.connectHandlers. + // They are inserted before our default handler. + var packageAndAppHandlers = connect(); + app.use(packageAndAppHandlers); + var suppressConnectErrors = false; // connect knows it is an error handler because it has 4 arguments instead of // 3. go figure. (It is not smart enough to find such a thing if it's hidden @@ -520,6 +523,7 @@ var runWebAppServer = function () { // start up app _.extend(WebApp, { connectHandlers: packageAndAppHandlers, + rawConnectHandlers: rawConnectHandlers, httpServer: httpServer, // metadata about the client program that we serve clientProgram: { diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index a52ccd54d2..cf9d026ea5 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,4 +1,7 @@ -=> Meteor 0.7.0.1: Fix failure to initialize local MongoDB server. +=> Meteor 0.7.1.1: Extend oplog tailing driver to support most common + MongoDB queries. Introduce Meteor developer accounts, a new way of + managing your meteor.com deployed sites. When you use `meteor + deploy`, you will be prompted to create a developer account. This release is being downloaded in the background. Update your - project to Meteor 0.7.0.1 by running 'meteor update'. + project to Meteor 0.7.1.1 by running 'meteor update'. diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 7a9375f70a..a7dfc4f8d5 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -78,6 +78,16 @@ { "release": "0.7.0.1" }, + { + "release": "0.7.1" + }, + { + "release": "0.7.1.1", + "packageNotices": { + "jquery": ["jquery has been upgraded to 1.11.0. See ", + "http://jquery.com/upgrade-guide/1.9/"] + } + }, { "release": "NEXT" } 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/bundler.js b/tools/bundler.js index a9beb901dd..0468cfb6d2 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -840,8 +840,16 @@ _.extend(ClientTarget.prototype, { _.each(originals, function (file, name) { if (! file.sourceMap) return; - newMap.applySourceMap( - new sourcemap.SourceMapConsumer(file.sourceMap), name); + try { + newMap.applySourceMap( + new sourcemap.SourceMapConsumer(file.sourceMap), name); + } catch (err) { + // If we can't apply the source map, silently drop it. + // + // XXX This is here because there are some less files that + // produce source maps that throw when consumed. We should + // figure out exactly why and fix it, but this will do for now. + } }); self.css[0].setSourceMap(JSON.stringify(newMap)); diff --git a/tools/commands.js b/tools/commands.js index 5992657887..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); @@ -773,7 +777,7 @@ main.registerCommand({ main.registerCommand({ name: 'deploy', - minArgs: 0, + minArgs: 1, maxArgs: 1, options: { 'delete': { type: Boolean, short: 'D' }, @@ -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 021c570051..6095545a7b 100644 --- a/tools/deploy-galaxy.js +++ b/tools/deploy-galaxy.js @@ -11,6 +11,7 @@ var auth = require('./auth.js'); var release = require('./release.js'); var url = require('url'); var _ = require('underscore'); +var buildmessage = require('./buildmessage.js'); // a bit of a hack var getPackage = _.once(function () { @@ -32,14 +33,14 @@ var handleError = function (error, galaxyName, messages) { var Package = getPackage(); messages = messages || {}; - if (e instanceof Package.meteor.Meteor.Error) { + if (error instanceof Package.meteor.Meteor.Error) { var msg = messages[error.error]; if (msg) process.stderr.write(msg + "\n"); else if (error.message) process.stderr.write("Denied: " + error.message + "\n"); return 1; - } else if (e instanceof ConnectionTimeoutError) { + } else if (error instanceof ConnectionTimeoutError) { // If we have an http/https URL for a galaxyName instead of a // proper galaxyName (which is what the code in this file // currently passes), strip off the scheme and trailing slash. @@ -150,7 +151,7 @@ _.extend(ServiceConnection.prototype, { } }); - self.subscribe.apply(self, args); + self.connection.subscribe.apply(self.connection, args); return fut.wait(); }, @@ -414,7 +415,7 @@ exports.deploy = function (options) { return 0; } finally { // Close the connection to Galaxy (otherwise Node will continue running). - conn.close(); + conn && conn.close(); } }; 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..43316989ed 100644 --- a/tools/help.txt +++ b/tools/help.txt @@ -144,8 +144,7 @@ Options: Reset the project state. Erases the local database. Usage: meteor reset -Reset the current project to a fresh state. Removes all local -data. +Reset the current project to a fresh state. Removes all local data. >>> deploy @@ -218,6 +217,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 +316,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 +387,3 @@ Grant a permission on an official service Usage: meteor admin grant [XXX] Not yet implemented - diff --git a/tools/main.js b/tools/main.js index f22f707da7..6261287e57 100644 --- a/tools/main.js +++ b/tools/main.js @@ -573,7 +573,19 @@ Fiber(function () { // appRelease will be null if a super old project with no // .meteor/release or 'none' if created by a checkout appRelease = project.getMeteorReleaseVersion(appDir); + // This is what happens if the file exists and is empty. This really + // shouldn't happen unless the user did it manually. + if (appRelease === '') { + process.stderr.write( +"Problem! This project has a .meteor/release file which is empty.\n" + +"The file should either contain the release of Meteor that you want to use,\n" + +"or the word 'none' if you will only use the project with unreleased\n" + +"checkouts of Meteor. Please edit the .meteor/release file in the project\n" + +"and change it to a valid Meteor release or 'none'.\n"); + process.exit(1); + } } + if (! files.usesWarehouse()) { // Running from a checkout if (releaseOverride) { @@ -909,7 +921,7 @@ commandName + ": You're not in a Meteor project directory.\n" + } if (command.requiresApp && release.current.isCheckout() && - appRelease !== "none") { + appRelease && appRelease !== "none") { // For commands that work with apps, if we have overridden the // app's usual release by using a checkout, print a reminder banner. process.stderr.write( diff --git a/tools/packages.js b/tools/packages.js index 88ae718d36..bca0e56012 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -336,7 +336,8 @@ _.extend(Slice.prototype, { // - appendDocument({ section: "head", data: "my markup" }) // Browser targets only. Add markup to the "head" or "body" // section of the document. - // - addStylesheet({ path: "my/stylesheet.css", data: "my css" }) + // - addStylesheet({ path: "my/stylesheet.css", data: "my css", + // sourceMap: "stringified json sourcemap"}) // Browser targets only. Add a stylesheet to the // document. 'path' is a requested URL for the stylesheet that // may or may not ultimately be honored. (Meteor will add @@ -453,7 +454,8 @@ _.extend(Slice.prototype, { resources.push({ type: "css", data: new Buffer(options.data, 'utf8'), - servePath: path.join(self.pkg.serveRoot, options.path) + servePath: path.join(self.pkg.serveRoot, options.path), + sourceMap: options.sourceMap }); }, addJavaScript: function (options) { diff --git a/tools/project.js b/tools/project.js index 45d4ca2ce8..b2aa5c126a 100644 --- a/tools/project.js +++ b/tools/project.js @@ -57,7 +57,8 @@ var meteorReleaseFilePath = function (appDir) { // This will return "none" if the project is not pinned to a release // (it was created by a checkout), or null for a legacy app with no -// .meteor/release file. +// .meteor/release file. It returns the empty string if the file exists +// but is empty. project.getMeteorReleaseVersion = function (appDir) { var releasePath = meteorReleaseFilePath(appDir); try { @@ -65,6 +66,9 @@ project.getMeteorReleaseVersion = function (appDir) { } catch (e) { return null; } + // This should really never happen, and the caller will print a special error. + if (!lines.length) + return ''; return trimLine(lines[0]); }; diff --git a/tools/selftest.js b/tools/selftest.js index bfabb4db87..082cc09760 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'); } }); @@ -839,7 +854,7 @@ _.extend(Run.prototype, { var net = require('net'); var lastStartTime = 0; - for (var attempts = 0; ! self.fakeMongoConnection && attempts < 20; + for (var attempts = 0; ! self.fakeMongoConnection && attempts < 50; attempts ++) { // Throttle attempts to one every 100ms utils.sleepMs((lastStartTime + 100) - (+ new Date)); @@ -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/test-utils.js b/tools/test-utils.js index ebfbf4fd01..0b6dfc2ae6 100644 --- a/tools/test-utils.js +++ b/tools/test-utils.js @@ -1,3 +1,8 @@ +var _ = require('underscore'); +var release = require('./release.js'); +var unipackage = require('./unipackage.js'); +var config = require('./config.js'); + var randomString = function (charsCount) { var chars = 'abcdefghijklmnopqrstuvwxyz'; var str = ''; @@ -7,6 +12,8 @@ var randomString = function (charsCount) { return str; }; +exports.accountsCommandTimeoutSecs = 15; + exports.randomString = randomString; var randomAppName = function () { @@ -63,12 +70,22 @@ exports.cleanUpLegacyApp = function (sandbox, name, password) { }; // Creates an app and deploys it. Assumes the sandbox is already logged -// in. -exports.createAndDeployApp = function (sandbox) { - var name = randomAppName(); - sandbox.createApp(name, 'empty'); +// in. Returns the name of the deployed app. Options: +// - settingsFile: a path to a settings file to deploy with +// - appName: app name to use; will be generated randomly if not +// provided +// - templateApp: the name of the template app to use. defaults to 'empty' +exports.createAndDeployApp = function (sandbox, options) { + options = options || {}; + var name = options.appName || randomAppName(); + sandbox.createApp(name, options.templateApp || 'empty'); sandbox.cd(name); - var run = sandbox.run('deploy', name); + var runArgs = ['deploy', name]; + if (options.settingsFile) { + runArgs.push('--settings'); + runArgs.push(options.settingsFile); + } + var run = sandbox.run.apply(sandbox, runArgs); run.waitSecs(90); run.match('Now serving at ' + name + '.meteor.com'); run.waitSecs(10); @@ -86,7 +103,7 @@ exports.cleanUpApp = function (sandbox, name) { exports.login = function (s, username, password) { var run = s.run('login'); - run.waitSecs(2); + run.waitSecs(15); run.matchErr('Username:'); run.write(username + '\n'); run.matchErr('Password:'); @@ -102,3 +119,67 @@ exports.logout = function (s) { run.matchErr('Logged out'); run.expectExit(0); }; + +var registrationUrlRegexp = + /https:\/\/www\.meteor\.com\/setPassword\?([a-zA-Z0-9\+\/]+)/; +exports.registrationUrlRegexp = registrationUrlRegexp; + +// In the sandbox `s`, create and deploy a new app with an unregistered +// email address. Returns the registration token from the printed URL in +// the deploy message. +exports.deployWithNewEmail = function (s, email, appName) { + s.createApp('deployapp', 'empty'); + s.cd('deployapp'); + var run = s.run('deploy', appName); + run.waitSecs(exports.accountsCommandTimeoutSecs); + run.matchErr('Email:'); + run.write(email + '\n'); + run.waitSecs(90); + // Check that we got a prompt to set a password on meteor.com. + run.matchErr('set a password'); + var urlMatch = run.matchErr(registrationUrlRegexp); + if (! urlMatch || ! urlMatch.length || ! urlMatch[1]) { + throw new Error("Missing registration token"); + } + var token = urlMatch[1]; + + run.expectExit(0); + + return token; +}; + +var getLoadedPackages = _.once(function () { + return unipackage.load({ + library: release.current.library, + packages: ['meteor', 'livedata'], + release: release.current.name + }); +}); + +var ddpConnect = function (url) { + var DDP = getLoadedPackages().livedata.DDP; + return DDP.connect(url); +}; + +exports.ddpConnect = ddpConnect; + +// Given a registration token created by doing a deferred registration +// with `email`, makes a DDP connection to the accounts server and +// finishes the registration process. +exports.registerWithToken = function (token, username, password, email) { + // XXX It might make more sense to hard-code the DDP url to + // https://www.meteor.com, since that's who the sandboxes are talking + // to. + var accountsConn = ddpConnect(config.getAuthDDPUrl()); + var registrationTokenInfo = accountsConn.call('registrationTokenInfo', + token); + var registrationCode = registrationTokenInfo.code; + accountsConn.call('register', { + username: username, + password: password, + emails: [email], + token: token, + code: registrationCode + }); + accountsConn.close(); +}; diff --git a/tools/tests/authorized.js b/tools/tests/authorized.js index fa31d0e067..8ea31dfa1b 100644 --- a/tools/tests/authorized.js +++ b/tools/tests/authorized.js @@ -2,7 +2,7 @@ var selftest = require('../selftest.js'); var testUtils = require('../test-utils.js'); var Sandbox = selftest.Sandbox; -var commandTimeoutSecs = 15; +var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs; var loggedInError = function(run) { run.waitSecs(commandTimeoutSecs); @@ -24,6 +24,9 @@ selftest.define("authorized", ['net', 'slow'], function () { run.matchErr("You must be logged in for that."); run.expectExit(1); + run = s.run("authorized"); + run.matchErr("not enough arguments"); + run = s.run("authorized", appName, "--remove", "bob"); run.waitSecs(commandTimeoutSecs); run.matchErr("You must be logged in for that."); @@ -116,3 +119,26 @@ selftest.define("authorized", ['net', 'slow'], function () { testUtils.cleanUpApp(s, appName); }); + +selftest.define('authorized - no username', ['net', 'slow'], function () { + var s = new Sandbox; + + // We shouldn't be able to add authorized users before we set a + // username. + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + var token = testUtils.deployWithNewEmail(s, email, appName); + var run = s.run('authorized', appName, '--add', 'test'); + run.waitSecs(commandTimeoutSecs); + run.matchErr('You must set a password on your account before ' + + 'you can authorize other users'); + run.expectExit(1); + // After we set a username, we should be able to authorize others. + testUtils.registerWithToken(token, username, 'testtest', email); + run = s.run('authorized', appName, '--add', 'test'); + run.waitSecs(commandTimeoutSecs); + run.match(': added test'); + run.expectExit(0); + testUtils.cleanUpApp(s, appName); +}); diff --git a/tools/tests/claim.js b/tools/tests/claim.js index ad8139b4d0..cde0984170 100644 --- a/tools/tests/claim.js +++ b/tools/tests/claim.js @@ -1,8 +1,9 @@ var selftest = require('../selftest.js'); var testUtils = require('../test-utils.js'); var Sandbox = selftest.Sandbox; +var files = require('../files.js'); -var commandTimeoutSecs = 15; +var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs; var loggedInError = selftest.markStack(function(run) { run.waitSecs(commandTimeoutSecs); @@ -24,6 +25,11 @@ selftest.define("claim", ['net', 'slow'], function () { var run = s.run('claim', testUtils.randomAppName(20)); loggedInError(run); + // Can't claim sites without specifying a site + run = s.run('claim'); + run.matchErr('not enough arguments'); + run.expectExit(1); + // Existing site. run = s.run('claim', 'mother-test'); loggedInError(run); @@ -46,6 +52,7 @@ selftest.define("claim", ['net', 'slow'], function () { testUtils.login(s, "test", "testtest"); run = s.run('authorized', appName, '--add', 'testtest'); run.waitSecs(commandTimeoutSecs); + run.match('added'); run.expectExit(0); testUtils.logout(s); testUtils.login(s, "testtest", "testtest"); @@ -55,13 +62,18 @@ selftest.define("claim", ['net', 'slow'], function () { testUtils.cleanUpApp(s, appName); // Legacy sites. - var sLegacy = new Sandbox({ - // Include a warehouse argument so that we can deploy apps with - // --release arguments. - warehouse: { - v1: { tools: 'tool1', latest: true } - } - }); + var sLegacy; + if (files.inCheckout()) { + sLegacy = new Sandbox({ + // Include a warehouse argument so that we can deploy apps with + // --release arguments. + warehouse: { + v1: { tools: 'tool1', latest: true } + } + }); + } else { + sLegacy = new Sandbox; + } // legacy w/pwd. var pwd = testUtils.randomString(10); @@ -99,3 +111,48 @@ selftest.define("claim", ['net', 'slow'], function () { testUtils.cleanUpApp(s, legacyApp); }); + +selftest.define('claim - no username', ['net', 'slow'], function () { + var s = new Sandbox; + var sandboxWithWarehouse; + if (files.inCheckout()) { + sandboxWithWarehouse = new Sandbox({ + // Include a warehouse argument so that we can deploy apps with + // --release arguments. + warehouse: { + v1: { tools: 'tool1', latest: true } + } + }); + } else { + sandboxWithWarehouse = new Sandbox; + } + + // We shouldn't be able to claim sites before we set a username. + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + var token = testUtils.deployWithNewEmail(s, email, appName); + var legacyAppName = testUtils.createAndDeployLegacyApp( + sandboxWithWarehouse, + 'test' + ); + var run = s.run('claim', legacyAppName); + run.waitSecs(commandTimeoutSecs); + run.matchErr('Password:'); + run.write('test\n'); + run.waitSecs(commandTimeoutSecs); + 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. + testUtils.registerWithToken(token, username, 'testtest', email); + run = s.run('claim', legacyAppName); + run.waitSecs(commandTimeoutSecs); + run.matchErr('Password: '); + run.write('test\n'); + run.waitSecs(commandTimeoutSecs); + run.match('transferred to your account'); + run.expectExit(0); + testUtils.cleanUpApp(s, appName); + testUtils.cleanUpApp(s, legacyAppName); +}); diff --git a/tools/tests/deploy-auth.js b/tools/tests/deploy-auth.js index 6c3759e511..f1c193aa60 100644 --- a/tools/tests/deploy-auth.js +++ b/tools/tests/deploy-auth.js @@ -1,22 +1,97 @@ var _ = require('underscore'); 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; + + // Deploy with no app name should fail + var run = s.run('deploy'); + run.matchErr('not enough arguments'); + run.expectExit(1); + + // Deploy outside of an app directory + run = s.run('deploy', testUtils.randomAppName()); + run.matchErr('not in a Meteor project directory'); + run.expectExit(1); +}); selftest.define('deploy - logged in', ['net', 'slow'], function () { // Create two sandboxes: one with a warehouse so that we can run // --release, and one without a warehouse so that we run from the // checkout or release that we started from. // XXX Is having two sandboxes the only way to do this? - var sandboxWithWarehouse = new Sandbox({ - // Include a warehouse arugment so that we can deploy apps with - // --release arguments. - warehouse: { - v1: { tools: 'tool1', latest: true } - } - }); - var sandbox = new Sandbox; + var sandboxWithWarehouse; + if (files.inCheckout()) { + sandboxWithWarehouse = new Sandbox({ + // Include a warehouse arugment so that we can deploy apps with + // --release arguments. + warehouse: { + v1: { tools: 'tool1', latest: true } + } + }); + } else { + sandboxWithWarehouse = new Sandbox; + } sandbox.createApp('deployapp', 'empty'); sandbox.cd('deployapp'); @@ -35,7 +110,7 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () { run.expectExit(0); // And we should have claimed the app by deploying to it. run = sandbox.run('claim', noPasswordLegacyApp); - run.waitSecs(20); + run.waitSecs(commandTimeoutSecs); run.matchErr('already belongs to you'); run.expectExit(1); // Clean up @@ -48,15 +123,15 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () { ); // We shouldn't be able to deploy to this app without claiming it run = sandbox.run('deploy', passwordLegacyApp); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('meteor claim'); run.expectExit(1); // If we claim it, we should be able to deploy to it. run = sandbox.run('claim', passwordLegacyApp); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('Password:'); run.write('test\n'); - run.waitSecs(10); + run.waitSecs(commandTimeoutSecs); run.match('successfully transferred to your account'); run.expectExit(0); run = sandbox.run('deploy', passwordLegacyApp); @@ -75,7 +150,7 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () { testUtils.logout(sandbox); testUtils.login(sandbox, 'testtest', 'testtest'); run = sandbox.run('deploy', appName); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('belongs to a different user'); run.expectExit(1); @@ -89,9 +164,14 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () { selftest.define('deploy - logged out', ['net', 'slow'], function () { var s = new Sandbox; - var sandboxWithWarehouse = new Sandbox({ - warehouse: { v1: { tools: 'tool1', latest: true } } - }); + var sandboxWithWarehouse; + if (files.inCheckout()) { + sandboxWithWarehouse = new Sandbox({ + warehouse: { v1: { tools: 'tool1', latest: true } } + }); + } else { + sandboxWithWarehouse = new Sandbox; + } testUtils.login(s, 'test', 'testtest'); var appName = testUtils.createAndDeployApp(s); @@ -100,10 +180,10 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () { // Deploy when logged out. We should be prompted to log in and then // the deploy should succeed. var run = s.run('deploy', appName); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Email:'); run.write('test@test.com\n'); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Password:'); run.write('testtest\n'); run.waitSecs(90); @@ -119,10 +199,10 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () { sandboxWithWarehouse ); run = s.run('deploy', legacyNoPassword); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('Email:'); run.write('test@test.com\n'); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('Password: '); run.write('testtest\n'); run.waitSecs(90); @@ -137,15 +217,15 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () { 'test' ); run = s.run('deploy', legacyPassword); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Email:'); // Log in with a username here to test that the email prompt also // accepts emails. (We put an email in the email prompt above.) run.write('test\n'); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Password:'); run.write('testtest\n'); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('meteor claim'); run.expectExit(1); @@ -158,12 +238,12 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () { appName = testUtils.randomAppName(); var email = testUtils.randomUserEmail(); run = s.run('deploy', appName); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Email:'); run.write(email + '\n'); run.waitSecs(90); run.match('Now serving'); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.expectExit(0); // Now that we've created an account with this email address, we // should be logged in as it and should be able to delete it. @@ -172,11 +252,11 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () { // Now that we've created a user, try to deploy a new app. appName = testUtils.randomAppName(); run = s.run('deploy', appName); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr('Email:'); run.write(email + '\n'); - run.waitSecs(5); - run.matchErr('already in use'); + run.waitSecs(commandTimeoutSecs); run.matchErr('pick a password'); + run.matchErr('An email has been sent to you with the link'); run.stop(); }); diff --git a/tools/tests/deploy-settings.js b/tools/tests/deploy-settings.js new file mode 100644 index 0000000000..c5dd02c319 --- /dev/null +++ b/tools/tests/deploy-settings.js @@ -0,0 +1,82 @@ +var _ = require('underscore'); +var selftest = require('../selftest.js'); +var testUtils = require('../test-utils.js'); +var utils = require('../utils.js'); +var Sandbox = selftest.Sandbox; +var httpHelpers = require('../http-helpers.js'); + +// Poll the given app looking for the correct settings. Throws an error +// if the settings aren't found after a timeout. +var checkForSettings = function (appName, settings, timeoutSecs) { + var timer = setTimeout(function () { + throw new Error('Expected settings not found on app ', appName); + }, timeoutSecs * 1000); + while (true) { + var result = httpHelpers.request('http://' + appName + '.meteor.com'); + + // XXX This is brittle; the test will break if we start formatting the + // __meteor_runtime_config__ JS differently. Ideally we'd do something + // like point a phantom at the deployed app and actually evaluate + // Meteor.settings. + var configRegexp = /__meteor_runtime_config__ = (.+);<\/script>/; + var configMatch = result.body.match(configRegexp); + if (configMatch && configMatch[1]) { + var stringifiedConfig = configMatch[1].trim(); + var parsedConfig = JSON.parse(stringifiedConfig); + if (_.isEqual(parsedConfig.PUBLIC_SETTINGS, settings['public'])) { + clearTimeout(timer); + return; + } + } + } +}; + +selftest.define('deploy - with settings', ['net', 'slow'], function () { + var s = new Sandbox; + testUtils.login(s, 'test', 'testtest'); + var settings = { + 'public': { a: 'b' } + }; + s.write('settings.json', JSON.stringify(settings)); + + // Deploy an app with settings and check that the public settings + // appear in the HTTP response body. + var appName = testUtils.createAndDeployApp(s, { + // Use standard-app instead of empty because we actually want + // standard-app-packages (including webapp) so that we can send a + // HTTP request to the app and get a response. + templateApp: 'standard-app', + // The path is ../settings.json instead of settings.json because + // createAndDeployApp creates a new app directory and cd's into it. + settingsFile: '../settings.json' + }); + checkForSettings(appName, settings, 10); + + // Re-deploy without settings and check that the settings still + // appear. + s.cd('..'); + testUtils.createAndDeployApp(s, { + templateApp: 'standard-app', + appName: appName + }); + // It takes a few seconds for the app to actually update, and we don't + // want to get a false positive in the meantime (i.e., if the settings + // disappear, we don't want to send our request before the app has + // updated and conclude that the settings are still there). + utils.sleepMs(5000); + checkForSettings(appName, settings, 10); + + // Re-deploy with new settings and check that the settings get + // updated. + settings['public'].a = 'c'; + s.cd('..'); + s.write('settings.json', JSON.stringify(settings)); + testUtils.createAndDeployApp(s, { + templateApp: 'standard-app', + settingsFile: '../settings.json', + appName: appName + }); + checkForSettings(appName, settings, 10); + + testUtils.cleanUpApp(s, appName); +}); diff --git a/tools/tests/login.js b/tools/tests/login.js index 6862048982..25b3dc0ecb 100644 --- a/tools/tests/login.js +++ b/tools/tests/login.js @@ -1,5 +1,8 @@ var selftest = require('../selftest.js'); var Sandbox = selftest.Sandbox; +var testUtils = require('../test-utils.js'); + +var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs; selftest.define("login", ['net'], function () { var s = new Sandbox; @@ -11,12 +14,46 @@ 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"); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr("Logged in as test."); run.expectExit(0); @@ -28,12 +65,12 @@ selftest.define("login", ['net'], function () { run.expectExit(0); run = s.run("logout"); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr("Logged out"); run.expectExit(0); run = s.run("logout"); - run.waitSecs(1); + run.waitSecs(commandTimeoutSecs); run.matchErr("Not logged in"); run.expectExit(0); @@ -47,7 +84,7 @@ selftest.define("login", ['net'], function () { run.write("test\n"); run.matchErr("Password:"); run.write("badpassword\n"); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr("Login failed"); run.expectExit(1); @@ -58,12 +95,12 @@ selftest.define("login", ['net'], function () { run.write("TeSt\n"); run.matchErr("Password:"); run.write("testtest\n"); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr("Logged in as test."); run.expectExit(0); run = s.run("logout"); - run.waitSecs(2); + run.waitSecs(commandTimeoutSecs); run.matchErr("Logged out"); run.expectExit(0); @@ -74,7 +111,29 @@ selftest.define("login", ['net'], function () { run.write("test\n"); run.matchErr("Password:"); run.write("TesTTesT\n"); - run.waitSecs(5); + run.waitSecs(commandTimeoutSecs); run.matchErr("Login failed"); run.expectExit(1); }); + +selftest.define('whoami - no username', ['net', 'slow'], function () { + var s = new Sandbox; + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + var token = testUtils.deployWithNewEmail(s, email, appName); + + var run = s.run('whoami'); + run.waitSecs(commandTimeoutSecs); + run.matchErr('You haven\'t chosen your username yet'); + run.matchErr(testUtils.registrationUrlRegexp); + run.expectExit(1); + testUtils.registerWithToken(token, username, 'test', email); + + run = s.run('whoami'); + run.waitSecs(commandTimeoutSecs); + run.read(username); + run.expectExit(0); + + testUtils.cleanUpApp(s, appName); +}); diff --git a/tools/tests/logs-mongo-auth.js b/tools/tests/logs-mongo-auth.js index 47f5abf0ab..8ab2358914 100644 --- a/tools/tests/logs-mongo-auth.js +++ b/tools/tests/logs-mongo-auth.js @@ -1,6 +1,7 @@ var _ = require('underscore'); var selftest = require('../selftest.js'); var Sandbox = selftest.Sandbox; +var testUtils = require('../test-utils.js'); // XXX need to make sure that mother doesn't clean up: // 'legacy-password-app-for-selftest' @@ -8,6 +9,8 @@ var Sandbox = selftest.Sandbox; // 'app-for-selftest-not-test-owned' // 'app-for-selftest-test-owned' +var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs; + // Run 'meteor logs' or 'meteor mongo' against an app. Options: // - legacy: boolean @@ -37,7 +40,7 @@ var logsOrMongoForApp = function (sandbox, command, appName, options) { } var run = sandbox.run.apply(sandbox, runArgs); - run.waitSecs(10); + run.waitSecs(commandTimeoutSecs); var expectSuccess = selftest.markStack(function () { run.match(matchString); @@ -68,11 +71,26 @@ 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(15); + run.waitSecs(commandTimeoutSecs); if (options.authorized) { expectSuccess(); } else { @@ -90,18 +108,33 @@ _.each([false, true], function (loggedIn) { ['net'], function () { var s = new Sandbox; + var run; if (loggedIn) { - var run = s.run('login'); - run.waitSecs(2); + run = s.run('login'); + run.waitSecs(commandTimeoutSecs); run.matchErr('Username:'); run.write('test\n'); run.matchErr('Password:'); run.write('testtest\n'); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('Logged in as test.'); run.expectExit(0); } + // Running 'meteor logs' without an app name should fail. + if (command === 'logs') { + run = s.run(command); + run.matchErr('not enough arguments'); + run.expectExit(1); + } + // Running 'meteor mongo' without an app name and not in an app + // dir should fail. + if (command === 'mongo') { + run = s.run('mongo'); + run.matchErr('not in a Meteor project directory'); + run.expectExit(1); + } + logsOrMongoForApp(s, command, 'legacy-no-password-app-for-selftest', { legacy: true, @@ -119,14 +152,15 @@ _.each([false, true], function (loggedIn) { logsOrMongoForApp(s, command, 'app-for-selftest-not-test-owned', { loggedIn: loggedIn, - authorized: false + authorized: false, + testReprompt: true }); if (! loggedIn) { // We logged in as a result of running the previous command, // so log out again. run = s.run('logout'); - run.waitSecs(15); + run.waitSecs(commandTimeoutSecs); run.matchErr('Logged out'); run.expectExit(0); } diff --git a/tools/tests/npm.js b/tools/tests/npm.js index f9998a6c53..07a53f45ef 100644 --- a/tools/tests/npm.js +++ b/tools/tests/npm.js @@ -25,11 +25,13 @@ selftest.define("npm", ["net"], function () { run.tellMongo(MONGO_LISTENING); if (i === 0) { run.waitSecs(2); + // use match instead of read because on a built release we can + // also get an update message here. run.read( "npm-test: updating npm dependencies -- meteor-test-executable...\n"); } run.waitSecs(15); - run.read("null; From shell script\n"); + run.match("null; From shell script\n"); run.expectEnd(); run.expectExit(0); }); diff --git a/tools/tests/registration.js b/tools/tests/registration.js index d740400608..7698fa2b4d 100644 --- a/tools/tests/registration.js +++ b/tools/tests/registration.js @@ -4,21 +4,20 @@ var testUtils = require('../test-utils.js'); var utils = require('../utils.js'); var Sandbox = selftest.Sandbox; var httpHelpers = require('../http-helpers.js'); -var release = require('../release.js'); -var unipackage = require('../unipackage.js'); var config = require('../config.js'); -var getLoadedPackages = _.once(function () { - return unipackage.load({ - library: release.current.library, - packages: ['meteor', 'livedata'], - release: release.current.name - }); -}); - -var ddpConnect = function (url) { - var DDP = getLoadedPackages().livedata.DDP; - return DDP.connect(url); +var expectInvalidToken = function (token) { + // Same XXX as testUtils.registerWithToken: should be hardcoded to + // https://www.meteor.com? + var accountsConn = testUtils.ddpConnect(config.getAuthDDPUrl()); + var registrationTokenInfo = accountsConn.call('registrationTokenInfo', + token); + // We should not be able to get a registration code for an invalid + // token. + if (registrationTokenInfo.valid || registrationTokenInfo.code) { + throw new Error('Expected invalid token is valid!'); + } + accountsConn.close(); }; // Polls a guerrillamail.com inbox every 3 seconds looking for an email @@ -96,12 +95,61 @@ var waitForEmail = selftest.markStack(function (inbox, subjectRegExp, return match; }); -selftest.define('deferred registration', ['net'], function () { +selftest.define('deferred registration - email registration token', ['net', 'slow'], function () { + var s = new Sandbox; + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + + var apiToken = testUtils.deployWithNewEmail(s, email, appName); + + // Check that we got a registration email in our inbox. + var registrationEmail = waitForEmail(email, /Set a password/, + /set a password/, 60); + + // Fish out the registration token and use to it to complete + // registration. + var token = testUtils.registrationUrlRegexp.exec(registrationEmail.bodyPage); + if (! token || ! token[1]) { + throw new Error("No registration token in email"); + } + token = token[1]; + + testUtils.registerWithToken(token, username, 'testtest', email); + + // 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'); + + // And after logging out and logging back in, we should have + // authorization to delete our app. + testUtils.cleanUpApp(s, appName); + + // All the tokens we got should now be invalid. + expectInvalidToken(token); + expectInvalidToken(apiToken); + + // 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'); - // Deploy an app with a new email address. + // 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(); @@ -110,52 +158,114 @@ selftest.define('deferred registration', ['net'], function () { run.matchErr('Email:'); run.write(email + '\n'); run.waitSecs(90); - // Check that we got a prompt to set a password on meteor.com. - run.matchErr('set a password'); - run.matchErr('https://www.meteor.com'); + run.match('Deploying'); + run.waitSecs(15); // because the bundler doesn't yield + 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); - // Check that we got a registration email in our inbox. - var registrationEmail = waitForEmail(email, /Set a password/, - /set a password/, 60); - - // Fish out the registration token and use to it to complete - // registration. - var token = /\/setPassword\?([a-zA-Z0-9\+\/]+)/. - exec(registrationEmail.bodyPage); - if (! token || ! token[1]) { - throw new Error("No registration token in email"); - } - token = token[1]; - - // XXX It might make more sense to hard-code the DDP url to - // https://www.meteor.com, since that's who the sandboxes are talking - // to. - var accountsConn = ddpConnect(config.getAuthDDPUrl()); - var registrationTokenInfo = accountsConn.call('registrationTokenInfo', - token); - var registrationCode = registrationTokenInfo.code; - accountsConn.call('register', { - username: username, - password: 'testtest', - emails: [email], - token: token, - code: registrationCode - }); - accountsConn.close(); - - // Success! We should be able to log out and log back in with our new - // password. - testUtils.logout(s); - testUtils.login(s, username, 'testtest'); - - // And after logging out and logging back in, we should have - // authorization to delete our app. - testUtils.cleanUpApp(s, appName); - - // XXX Test the api registration URLs - // XXX Test that registration URLs get printed when they should - // XXX Test registration while the tool is waiting on a DDP method to - // return (e.g. deploy and login with an existing username that - // doesn't have a password set yet) + // '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'], + function () { + var s = new Sandbox; + + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + var token = testUtils.deployWithNewEmail(s, email, appName); + testUtils.registerWithToken(token, username, 'testtest', email); + + testUtils.logout(s); + testUtils.login(s, username, 'testtest'); + testUtils.cleanUpApp(s, appName); + + // All tokens we received should not be invalid. + expectInvalidToken(token); + var registrationEmail = waitForEmail(email, /Set a password/, + /set a password/, 60); + var emailToken = testUtils.registrationUrlRegexp.exec( + registrationEmail.bodyPage + ); + if (! emailToken || ! emailToken[1]) { + throw new Error('No registration token in email'); + } + expectInvalidToken(emailToken[1]); + } +); + +selftest.define( + 'deferred registration - register after logging out', + ['net', 'slow'], + function () { + var s = new Sandbox; + var email = testUtils.randomUserEmail(); + var username = testUtils.randomString(10); + var appName = testUtils.randomAppName(); + var token = testUtils.deployWithNewEmail(s, email, appName); + testUtils.logout(s); + + // If we deploy again with the same email address after logging out, + // we should get a message telling us to check our email and + // register, and the tool should obediently wait for us to do that + // before doing the deploy. + s.createApp('deployapp2', 'empty'); + s.cd('deployapp2'); + var run = s.run('deploy', appName); + run.waitSecs(testUtils.accountsCommandTimeoutSecs); + run.matchErr('Email:'); + run.write(email + '\n'); + run.waitSecs(testUtils.accountsCommandTimeoutSecs); + run.matchErr('pick a password'); + run.matchErr('Waiting for you to register on the web...'); + + var registrationEmail = waitForEmail( + email, + /Set a password/, + /You previously created a Meteor developer account/, + 60 + ); + token = testUtils.registrationUrlRegexp.exec( + registrationEmail.bodyPage + ); + if (! token || ! token[1]) { + throw new Error('No registration token in email'); + } + + testUtils.registerWithToken(token[1], username, 'testtest', email); + run.waitSecs(testUtils.accountsCommandTimeoutSecs); + run.matchErr('Username: ' + username + '\n'); + run.matchErr('Password: '); + run.write('testtest\n'); + run.waitSecs(90); + run.match('Now serving at'); + run.expectExit(0); + + run = s.run('whoami'); + run.read(username); + run.expectExit(0); + + testUtils.cleanUpApp(s, appName); + } +); diff --git a/tools/tests/releases.js b/tools/tests/releases.js index 46830de2cc..da70a2de0a 100644 --- a/tools/tests/releases.js +++ b/tools/tests/releases.js @@ -92,6 +92,12 @@ selftest.define("springboard", ['checkout'], function () { run.read('v2\ntools2\n'); run.expectEnd(); run.expectExit(0); + + // .meteor/release exists but is empty. You get an error. + s.write(".meteor/release", "\n"); + run = s.run("list", "--using"); + run.matchErr("release file which is empty"); + run.expectExit(1); }); }); diff --git a/tools/tests/run.js b/tools/tests/run.js index 68a204aae8..db0ff667e4 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -44,6 +44,7 @@ selftest.define("run", function () { // Crashes s.write("crash.js", "process.exit(42);"); + run.waitSecs(5); run.match("with code: 42"); run.waitSecs(5); run.match("is crashing"); @@ -171,7 +172,7 @@ selftest.define("run --once", function () { s.cd("onceapp"); s.set("RUN_ONCE_OUTCOME", "mongo"); run = s.run("--once"); - run.waitSecs(5); + run.waitSecs(15); run.expectExit(86); }); diff --git a/tools/updater.js b/tools/updater.js index baf79fdd22..e449bb5f3b 100644 --- a/tools/updater.js +++ b/tools/updater.js @@ -71,9 +71,9 @@ var check = function (showBanner) { if (manifest.releases.stable.banner && warehouse.lastPrintedBannerRelease() !== manifestLatestRelease) { if (showBanner) { - runLog.log(); + runLog.log(""); runLog.log(manifest.releases.stable.banner); - runLog.log(); + runLog.log(""); } warehouse.writeLastPrintedBannerRelease(manifestLatestRelease); } else { diff --git a/tools/warehouse.js b/tools/warehouse.js index be6c6015da..999d28cce4 100644 --- a/tools/warehouse.js +++ b/tools/warehouse.js @@ -341,11 +341,24 @@ _.extend(warehouse, { // (whether or not we just downloaded them). (Don't do this if we didn't // print the installing message!) if (newPieces && showInstalling) { + var unlinkIfExists = function (file) { + try { + fs.unlinkSync(file); + } catch (e) { + // If two processes populate the warehouse in parallel, the other + // process may have deleted the fresh file. That's OK! + if (e.code === "ENOENT") + return; + throw e; + } + }; + if (newPieces.tools) { - fs.unlinkSync(warehouse.getToolsFreshFile(newPieces.tools.version)); + unlinkIfExists(warehouse.getToolsFreshFile(newPieces.tools.version)); } _.each(newPieces.packages, function (packageInfo, name) { - fs.unlinkSync(warehouse.getPackageFreshFile(name, packageInfo.version)); + unlinkIfExists( + warehouse.getPackageFreshFile(name, packageInfo.version)); }); }