yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #02114
[Merge] lp:~benji/juju-gui/login into lp:juju-gui
Benji York has proposed merging lp:~benji/juju-gui/login into lp:juju-gui.
Requested reviews:
Juju GUI Hackers (juju-gui)
Related bugs:
Bug #1074423 in juju-gui: "Enable user/password login"
https://bugs.launchpad.net/juju-gui/+bug/1074423
For more details, see:
https://code.launchpad.net/~benji/juju-gui/login/+merge/141133
Add user login support.
Future work: we need to implement a real UI for this. At the moment this
branch uses prompt() calls to generate modal dialogs. Also, this will
probably need slight tweaking once the backend actually requires
authorization.
https://codereview.appspot.com/7007047/
--
https://code.launchpad.net/~benji/juju-gui/login/+merge/141133
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~benji/juju-gui/login into lp:juju-gui.
=== modified file 'app/app.js'
--- app/app.js 2012-12-19 13:45:10 +0000
+++ app/app.js 2012-12-21 20:53:25 +0000
@@ -29,6 +29,11 @@
preserve: true
},
+ login: {
+ type: 'juju.views.login',
+ preserve: true
+ },
+
service: {
type: 'juju.views.service',
preserve: false,
@@ -517,6 +522,32 @@
},
/**
+ * Ensure that the current user has authenticated.
+ *
+ * @method check_user_credentials
+ * @param {Object} req The request.
+ * @param {Object} res Something. ???
+ * @param {Object} res ???
+ * @param {Object} next The next route handler.
+ *
+ */
+ check_user_credentials: function(req, res, next) {
+ var viewInfo = this.getViewInfo('login');
+ if (!viewInfo.instance) {
+ viewInfo.instance = new views.LoginView({
+ env: this.env
+ });
+ }
+ var view = viewInfo.instance;
+ // If there has not been a successful login attempt, prompt for
+ // credentials.
+ if (!view.waiting && !view.userIsAuthenticated) {
+ view.login();
+ }
+ next();
+ },
+
+ /**
* Display the provider type.
*
* The provider type arrives asynchronously. Instead of updating the
@@ -709,6 +740,7 @@
charm_store: {},
routes: {
value: [
+ { path: '*', callback: 'check_user_credentials'},
{ path: '*', callback: 'show_notifications_view'},
{ path: '/charms/', callback: 'show_charm_collection'},
{ path: '/charms/*charm_store_path',
@@ -746,6 +778,7 @@
}, '0.5.2', {
requires: [
'juju-models',
+ 'juju-notification-controller',
'juju-charm-models',
'juju-views',
'juju-controllers',
=== modified file 'app/modules-debug.js'
--- app/modules-debug.js 2012-12-21 12:52:30 +0000
+++ app/modules-debug.js 2012-12-21 20:53:25 +0000
@@ -96,6 +96,10 @@
fullpath: '/juju-ui/views/environment.js'
},
+ 'juju-view-login': {
+ fullpath: '/juju-ui/views/login.js'
+ },
+
'juju-view-service': {
fullpath: '/juju-ui/views/service.js'
},
@@ -124,6 +128,7 @@
'juju-view-utils',
'juju-topology',
'juju-view-environment',
+ 'juju-view-login',
'juju-view-service',
'juju-view-unit',
'juju-view-charm',
=== modified file 'app/modules-prod.js'
--- app/modules-prod.js 2012-12-13 02:43:15 +0000
+++ app/modules-prod.js 2012-12-21 20:53:25 +0000
@@ -17,6 +17,7 @@
'juju-topology',
'juju-view-utils',
'juju-view-environment',
+ 'juju-view-login',
'juju-view-service',
'juju-view-unit',
'juju-view-charm',
=== modified file 'app/store/env.js'
--- app/store/env.js 2012-10-15 13:20:38 +0000
+++ app/store/env.js 2012-12-21 20:53:25 +0000
@@ -64,20 +64,19 @@
on_close: function(data) {
console.log('Env: Disconnect');
this.set('connected', false);
+ this.set('serverReady', false);
},
on_message: function(evt) {
console.log('Env: Receive', evt.data);
-
var msg = Y.JSON.parse(evt.data);
+ // The "ready" attribute indicates that this is a server's initial
+ // greeting. It provides a few initial values that we care about.
if (msg.ready) {
- // The "ready" attribute indicates that this is a server's initial
- // greeting. It provides a few initial values that we care about.
+ console.log('Env: Handshake Complete');
+ this.set('serverReady', true);
this.set('providerType', msg.provider_type);
this.set('defaultSeries', msg.default_series);
- }
- if (msg.version === 0) {
- console.log('Env: Handshake Complete');
return;
}
this.fire('msg', msg);
@@ -174,6 +173,18 @@
this._send_rpc({'op': 'expose', 'service_name': service}, callback);
},
+ /**
+ * Attempt to log the user in.
+ * @param {Object} user The user name.
+ * @param {Object} password The user's password.
+ * @param {Object} [callback] A function to call when a response to the
+ * message arrives.
+ * @return {undefined} Nothing.
+ */
+ login: function(user, password, callback) {
+ this._send_rpc({op: 'login', user: user, password: password}, callback);
+ },
+
unexpose: function(service, callback) {
this._send_rpc({'op': 'unexpose', 'service_name': service}, callback);
},
=== added file 'app/views/login.js'
--- app/views/login.js 1970-01-01 00:00:00 +0000
+++ app/views/login.js 2012-12-21 20:53:25 +0000
@@ -0,0 +1,124 @@
+'use strict';
+
+var no_login_prompts = false;
+
+YUI.add('juju-view-login', function(Y) {
+
+ var views = Y.namespace('juju.views');
+ var Templates = views.Templates;
+
+ var LoginView = Y.Base.create('LoginView', Y.View, [views.JujuBaseView], {
+ // This is so tests can easily determine if the user was prompted.
+ _prompted: false,
+
+ /**
+ * Initialize the login view.
+ *
+ * @return {undefined} Nothing.
+ */
+ initializer: function() {
+ this.userIsAuthenticated = false;
+ // When the server tells us the outcome of a login attempt we record
+ // the result.
+ this.get('env').after('log', this.handleLoginEvent, this);
+ },
+
+ /**
+ * React to the results of sennding a login message to the server.
+ *
+ * @method handleLoginEvent
+ * @param {Object} evt The event to which we are responding.
+ * @return {undefined} Nothing.
+ */
+ handleLoginEvent: function(evt) {
+ // We are only interested in the responses to login events.
+ if (evt.data.op !== 'login') {
+ return;
+ }
+ this.userIsAuthenticated = !!evt.data.result;
+ this.waiting = false;
+ },
+
+ /**
+ * Prompt the user for input.
+ *
+ * Does nothing if a special global flag has been set. This is used so
+ * tests do not generate prompts.
+ *
+ * @method _prompt
+ * @param {String} message The message to display to the user.
+ * @return {String} The string the user typed.
+ */
+ _prompt: function(message) {
+ // no_login_prompts is a global.
+ if (no_login_prompts) {
+ return null;
+ }
+ return window.prompt(message);
+ },
+
+ /**
+ * Prompt the user for their user name and password.
+ *
+ * @method promptUser
+ * @return {undefined} Nothing.
+ */
+ promptUser: function() {
+ this._prompted = true;
+ this.set('user', this._prompt('User name'));
+ this.set('password', this._prompt('Password'));
+ this.waiting = true;
+ },
+
+ /**
+ * Send a login RPC message to the server.
+ *
+ * @method validateCredentials
+ * @param {String} user The user name.
+ * @param {String} password The password.
+ * @return {undefined} Nothing.
+ */
+ validateCredentials: function(user, password) {
+ this.get('env').login(user, password);
+ },
+
+ /**
+ * Attempt to log the user in.
+ *
+ * If a login attempt is outstanding, this function is a no-op.
+ *
+ * @method login
+ * @return {undefined} Nothing.
+ */
+ login: function() {
+ // If the server connection is not yet ready, then there is no use in
+ // trying to authenticate.
+ var env = this.get('env');
+ if (!env.get('serverReady')) {
+ return;
+ }
+ // If the credentials are known good or we are waiting to find out, exit
+ // early.
+ if (this.userIsAuthenticated || this.waiting) {
+ return;
+ }
+ // If there are no stored credentials, prompt the user for some.
+ if (!Y.Lang.isValue(this.get('user'))) {
+ this.promptUser();
+ }
+ this.validateCredentials(this.get('user'), this.get('password'));
+ }
+
+ });
+
+ views.LoginView = LoginView;
+
+}, '0.1.0', {
+ requires: [
+ 'view',
+ 'juju-view-utils',
+ 'node',
+ 'handlebars'
+ ]
+});
+
=== modified file 'test/index.html'
--- test/index.html 2012-12-17 14:53:46 +0000
+++ test/index.html 2012-12-21 20:53:25 +0000
@@ -16,7 +16,31 @@
var assert = chai.assert,
expect = chai.expect
should = chai.should();
- mocha.setup({'ui': 'bdd', 'ignoreLeaks': false})
+ mocha.setup({ui: 'bdd', ignoreLeaks: false})
+ </script>
+
+ <script>
+ YUI().use('node', 'event', function(Y) {
+ no_login_prompts = true;
+ var config = GlobalConfig;
+ for (group in config.groups) {
+ var group = config.groups[group];
+ for (m in group.modules) {
+ var resource = group.modules[m];
+ if (!m || !resource.fullpath) {
+ continue
+ }
+ resource.fullpath = resource.fullpath.replace(
+ '/juju-ui/', '../juju-ui/', 1);
+ // If we load modules asyncronously then the module loading may take
+ // so long that the test definitions (and before/after calls) happen
+ // *after* the test runner is invoked. In which case the test runner
+ // will not know about the tests and therefore not run them.
+ resource.async = false;
+ }
+ }
+ Y.on('domready', mocha.run);
+ });
</script>
<script src="test_d3_components.js"></script>
@@ -32,6 +56,7 @@
<script src="test_service_config_view.js"></script>
<script src="test_service_view.js"></script>
<script src="test_utils.js"></script>
+ <script src="test_login_view.js"></script>
<script src="test_charm_panel.js"></script>
<script src="test_charm_configuration.js"></script>
<script src="test_console.js"></script>
@@ -41,27 +66,6 @@
<script src="test_app_hotkeys.js"></script>
<script src="test_notifier_widget.js"></script>
- <script>
- YUI().use('node', 'event', function(Y) {
- Y.on('domready', function() {
-
- var config = GlobalConfig;
- for (group in config.groups) {
- var group = config.groups[group];
- for (m in group.modules) {
- var resource = group.modules[m];
- if (!m || !resource.fullpath) {
- continue
- }
- resource.fullpath = resource.fullpath.replace(
- '/juju-ui/', '../juju-ui/', 1);
- }
- }
- // Load before test runner
- mocha.run();
- });
- });
- </script>
</head>
=== added file 'test/test_login_view.js'
--- test/test_login_view.js 1970-01-01 00:00:00 +0000
+++ test/test_login_view.js 2012-12-21 20:53:25 +0000
@@ -0,0 +1,225 @@
+'use strict';
+
+(function() {
+
+ var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
+ var Y = YUI(GlobalConfig).use(requires);
+
+ describe('login view', function() {
+ var conn, env, utils, juju, makeLoginView, views, app;
+ var test = it; // We aren't really doing BDD so let's be more direct.
+
+ before(function() {
+ views = Y.namespace('juju.views');
+ utils = Y.namespace('juju-tests.utils');
+ juju = Y.namespace('juju');
+ makeLoginView = function() {
+ var view = new views.LoginView({env: env});
+ return view;
+ };
+ });
+
+ beforeEach(function() {
+ var container = Y.Node.create('<div/>');
+ conn = new utils.SocketStub();
+ env = new juju.Environment({conn: conn});
+ env.connect();
+ conn.open();
+ env.set('serverReady', true);
+ });
+
+ afterEach(function() {
+ env.destroy();
+ });
+
+ test('login view can be instantiated', function() {
+ var view = makeLoginView();
+ });
+
+ test('credentials are initially assumed invalid', function() {
+ var view = makeLoginView();
+ assert.equal(view.userIsAuthenticated, false);
+ });
+
+ test('successful login event marks user as authenticated', function() {
+ var view = makeLoginView();
+ var evt = {data: {op: 'login', result: true}};
+ view.handleLoginEvent(evt);
+ assert.equal(view.userIsAuthenticated, true);
+ });
+
+ test('unsuccessful login event marks user as unauthenticated', function() {
+ var view = makeLoginView();
+ var evt = {data: {op: 'login'}};
+ view.handleLoginEvent(evt);
+ assert.equal(view.userIsAuthenticated, false);
+ });
+
+ test('credentials passed to the constructor are stored', function() {
+ var fauxEnvironment = {
+ after: function() {
+ }
+ };
+ var user = 'Will Smith';
+ var password = 'I am legend!';
+ var view = new views.LoginView({
+ user: user,
+ password: password,
+ env: fauxEnvironment
+ });
+ assert.equal(view.get('user'), user);
+ assert.equal(view.get('password'), password);
+ });
+
+ test('prompts when there are no known credentials', function() {
+ var view = makeLoginView();
+ view.login();
+ assert.isTrue(view._prompted);
+ });
+
+ test('no prompt when there are known credentials', function() {
+ var view = makeLoginView();
+ view.set('user', 'user');
+ view.set('password', 'password');
+ view.login();
+ assert.isFalse(view._prompted);
+ });
+
+ test('login() sends login message', function() {
+ var view = makeLoginView();
+ view.set('user', 'user');
+ view.set('password', 'password');
+ view.login();
+ assert.equal(conn.last_message().op, 'login');
+ });
+
+ });
+
+
+ describe('login user interaction', function() {
+ var conn, env, utils, juju, makeLoginView, views, app;
+ var test = it; // We aren't really doing BDD so let's be more direct.
+
+ before(function() {
+ views = Y.namespace('juju.views');
+ utils = Y.namespace('juju-tests.utils');
+ juju = Y.namespace('juju');
+ makeLoginView = function() {
+ var view = new views.LoginView({env: env});
+ return view;
+ };
+ });
+
+ beforeEach(function() {
+ var container = Y.Node.create('<div/>');
+ conn = new utils.SocketStub();
+ env = new juju.Environment({conn: conn});
+ env.connect();
+ conn.open();
+ env.set('serverReady', true);
+ app = new Y.juju.App({env: env, container: container});
+ });
+
+ afterEach(function() {
+ env.destroy();
+ });
+
+ test('the login checker is registered first for all routes', function() {
+ assert.equal(app.match('/')[0].callback, 'check_user_credentials');
+ });
+
+ test('unauthorized requests prompt for credentials', function() {
+ var view = app.getViewInfo('login').instance = makeLoginView();
+ app.check_user_credentials(undefined, undefined, function() {});
+ assert.isTrue(view._prompted);
+ });
+
+ test('user is not prompted while waiting for login results', function() {
+ var view = app.getViewInfo('login').instance = makeLoginView();
+ app.check_user_credentials(undefined, undefined, function() {});
+ assert.isTrue(view._prompted);
+ assert.isTrue(view.waiting);
+ view._prompted = false;
+ app.check_user_credentials(undefined, undefined, function() {});
+ assert.isFalse(view._prompted);
+ });
+
+ // If there are know credentials that are not known to be bad (they are
+ // either good or not yet validated) and a login request is made, no
+ // prompting is done, instead the existing credentials are reused, or if we
+ // are waiting on credential validation, no action is taken.
+ test('no prompting if non-invalid credentials are available', function() {
+ var view = app.getViewInfo('login').instance = makeLoginView();
+ app.check_user_credentials(undefined, undefined, function() {});
+ assert.isTrue(view._prompted);
+ assert.isTrue(view.waiting);
+ view._prompted = false;
+ app.check_user_credentials(undefined, undefined, function() {});
+ assert.isFalse(view._prompted);
+ });
+
+ });
+
+
+ describe('login server interaction', function() {
+ var conn, env, utils, juju, makeLoginView, views, app;
+ var test = it; // We aren't really doing BDD so let's be more direct.
+
+ before(function() {
+ views = Y.namespace('juju.views');
+ utils = Y.namespace('juju-tests.utils');
+ juju = Y.namespace('juju');
+ makeLoginView = function() {
+ var view = new views.LoginView({env: env});
+ return view;
+ };
+ });
+
+ beforeEach(function() {
+ var container = Y.Node.create('<div/>');
+ conn = new utils.SocketStub();
+ env = new juju.Environment({conn: conn});
+ env.connect();
+ conn.open();
+ env.set('serverReady', true);
+ app = new Y.juju.App({env: env, container: container});
+ });
+
+ afterEach(function() {
+ env.destroy();
+ });
+
+ test('the login method actually sends a message ', function() {
+ env.login('user', 'pass');
+ assert.equal(conn.last_message().op, 'login');
+ });
+
+ test('the view contacts the server to verify credentials', function() {
+ var view = makeLoginView();
+ var user = 'Will Smith';
+ var password = 'I am legend!';
+ // If we ask the view to validate credentials, a login message will be
+ // sent to the server.
+ view.validateCredentials(user, password);
+ var msg = conn.last_message();
+ assert.equal(msg.op, 'login');
+ assert.equal(msg.user, user);
+ assert.equal(msg.password, password);
+ });
+
+ test('successful credential verification messages are handled', function() {
+ var view = makeLoginView();
+ assert.isFalse(view.userIsAuthenticated);
+ env.fire('log', {data: {op: 'login', result: true}});
+ assert.isTrue(view.userIsAuthenticated);
+ });
+
+ test('failed credential verification messages are handled', function() {
+ var view = makeLoginView();
+ assert.isFalse(view.userIsAuthenticated);
+ env.fire('log', {op: 'login', data: {}});
+ assert.isFalse(view.userIsAuthenticated);
+ });
+
+ });
+})();
=== modified file 'test/test_service_view.js'
--- test/test_service_view.js 2012-11-08 18:16:07 +0000
+++ test/test_service_view.js 2012-12-21 20:53:25 +0000
@@ -23,7 +23,8 @@
env = new (Y.namespace('juju')).Environment({conn: conn});
env.connect();
conn.open();
- container = Y.Node.create('<div id="test-container" />');
+ container = Y.Node.create('<div/>')
+ .hide();
Y.one('#main').append(container);
db = new models.Database();
charm = new models.Charm({id: 'cs:precise/mysql-7', description: 'A DB'});
Follow ups