Merge lp:~gary/juju-gui/authtoken into lp:juju-gui/experimental
- authtoken
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1213 |
Proposed branch: | lp:~gary/juju-gui/authtoken |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
1162 lines (+790/-126) 11 files modified
app/app.js (+94/-19) app/store/env/fakebackend.js (+21/-0) app/store/env/go.js (+51/-3) app/store/env/sandbox.js (+32/-3) app/views/login.js (+8/-2) test/test_app.js (+186/-2) test/test_env_go.js (+208/-97) test/test_fakebackend.js (+31/-0) test/test_login.js (+1/-0) test/test_sandbox_go.js (+87/-0) test/utils.js (+71/-0) |
To merge this branch: | bzr merge lp:~gary/juju-gui/authtoken |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Add authtoken support to the GUI
The authtoken support in this branch includes support for both a real environment and the sandbox. In writing this and testing it, I encountered some code that was not working, and some code that was not tested, and some code that was very difficult to test. This branch is utterly gigantic, for which I apologize. It includes the changes I needed to get everything else working. I should factor it out, now that I have figured out what needs to be done, but I'm a bit fatigued, so I'm asking for reviewer indulgence.
There are two ways you should QA. First, in the sandbox, delete the user and password from the config-debug file and then load the GUI in your browser using a URL like this: http://
Now it's time to QA a live environment. Here's how I suggest you do it.
1. In this branch, run BRANCH_IS_GOOD=1 make distfile . When it is finished, it will tell you what file it made.
2. Get a copy of the lp:~juju-gui/charms/precise/juju-gui/trunk/ branch if you don't have one already. If you do have one, make sure it is up to date.
3. mv the file you made in step 1 to the charm's releases directory. rm the old release in that directory.
4. juju bootstrap.
5. In the charm, run make deploy. Wait until it says it is done deploying the code.
6. Start up Python in your local machine. Edit the following code to include the address from step 5 and the appropriate password from your ~/.juju/
import itertools
import json
import pprint
import websocket
address = 'PUBLICADDRESS' # e.g. ec2-107-
password = 'YOURPASSWORD'
url = 'wss://
ws = websocket.
counter = itertools.count()
def process(request):
request = request.copy()
request[
ws.
pprint.
process(
process(
The last response should be something like this:
{u'RequestId': 2,
u'Response': {u'Created': u'2013-
7. Now in your browser construct a url that has the GUI address from step 5 and the authtoken from step 6. It should look something like this:
https:/
Go to this address. It should log you in as it did in the sandbox.
8. Try logging in and out with different methods to see if everything works as you expect.
Thank you very much!!!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Minors, will QA next.
https:/
File app/app.js (right):
https:/
app/app.js:521: // proxy, withing an authenticated websocket session,
use a
*within
https:/
File app/store/env/go.js (right):
https:/
app/store/
token.
*was
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeff Pihach (hatch) wrote : | # |
LGTM very thorough thanks!
- 1214. By Gary Poster
-
respond to review
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
QA okay, LGTM from me
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
*** Submitted:
Add authtoken support to the GUI
The authtoken support in this branch includes support for both a real
environment and the sandbox. In writing this and testing it, I
encountered some code that was not working, and some code that was not
tested, and some code that was very difficult to test. This branch is
utterly gigantic, for which I apologize. It includes the changes I
needed to get everything else working. I should factor it out, now that
I have figured out what needs to be done, but I'm a bit fatigued, so I'm
asking for reviewer indulgence.
There are two ways you should QA. First, in the sandbox, delete the
user and password from the config-debug file and then load the GUI in
your browser using a URL like this:
http://
remove the authtoken from the URL, and notify you that you used a token
to authenticate. Find some other URLs, like in the charm browser, and
copy them. Log out with the button on the top right. Paste the
previous URL in to the browser, and then insert ?authtoken=
the URL. *Note that a querystring should come before a hash, so
?authtoken=
Maybe do that log in, log out cycle a couple of times to try a few
different URLs. Now log out and try a different token, like
?authtoken=
appropriate error message.
Now it's time to QA a live environment. Here's how I suggest you do it.
1. In this branch, run BRANCH_IS_GOOD=1 make distfile . When it is
finished, it will tell you what file it made.
2. Get a copy of the lp:~juju-gui/charms/precise/juju-gui/trunk/ branch
if you don't have one already. If you do have one, make sure it is up
to date.
3. mv the file you made in step 1 to the charm's releases directory. rm
the old release in that directory.
4. juju bootstrap.
5. In the charm, run make deploy. Wait until it says it is done
deploying the code.
6. Start up Python in your local machine. Edit the following code to
include the address from step 5 and the appropriate password from your
~/.juju/
import itertools
import json
import pprint
import websocket
address = 'PUBLICADDRESS' # e.g.
ec2-107-
password = 'YOURPASSWORD'
url = 'wss://
ws = websocket.
counter = itertools.count()
def process(request):
request = request.copy()
request[
ws.
pprint.
process(
'user-admin', 'Password': password}))
process(
The last response should be something like this:
{u'RequestId': 2,
u'Response': {u'Created': u'2013-
7. Now in your browser construct a url that has the GUI address from
step 5 and the authtoken from step 6. It should look something like
this:
https://...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Thanks again to both of you, and apologies for the size.
Preview Diff
1 | === modified file 'app/app.js' |
2 | --- app/app.js 2013-11-18 13:27:49 +0000 |
3 | +++ app/app.js 2013-11-26 22:03:26 +0000 |
4 | @@ -500,6 +500,9 @@ |
5 | }); |
6 | this.endpointsController.bind(); |
7 | |
8 | + // Stash the location object so that tests can override it. |
9 | + this.location = window.location; |
10 | + |
11 | // When the connection resets, reset the db, re-login (a delta will |
12 | // arrive with successful authentication), and redispatch. |
13 | this.env.after('connectedChange', function(ev) { |
14 | @@ -511,7 +514,33 @@ |
15 | if (credentials && credentials.areAvailable) { |
16 | this.env.login(); |
17 | } else { |
18 | - this.checkUserCredentials(); |
19 | + // The user can also try to log in with an authentication token. |
20 | + // This will look like ?authtoken=AUTHTOKEN. For instance, |
21 | + // in the sandbox, try logging in with ?authtoken=demoToken. |
22 | + // To get a real token from the Juju GUI charm's environment |
23 | + // proxy, within an authenticated websocket session, use a |
24 | + // request like this: |
25 | + // { |
26 | + // 'RequestId': 42, |
27 | + // 'Type': 'GUIToken', |
28 | + // 'Request': 'Create', |
29 | + // 'Params': {}, |
30 | + // } |
31 | + // You can then use the token once until it expires, within two |
32 | + // minutes of this writing. |
33 | + var querystring = this.location.search.substring(1); |
34 | + var qs = Y.QueryString.parse(querystring); |
35 | + var authtoken = qs.authtoken; |
36 | + if (Y.Lang.isValue(authtoken)) { |
37 | + // De-dupe if necessary. |
38 | + if (Y.Lang.isArray(authtoken)) { |
39 | + authtoken = authtoken[0]; |
40 | + } |
41 | + // Try a token login. |
42 | + this.env.tokenLogin(authtoken); |
43 | + } else { |
44 | + this.checkUserCredentials(); |
45 | + } |
46 | } |
47 | } |
48 | }, this); |
49 | @@ -895,6 +924,31 @@ |
50 | }, |
51 | |
52 | /** |
53 | + Get the path to which we should redirect after logging in. Clear it out |
54 | + afterwards so it is clear that we've consumed it. |
55 | + |
56 | + This is logic from the onLogin method factored out to make it easier to |
57 | + test. |
58 | + |
59 | + @method popLoginRedirectPath |
60 | + @private |
61 | + @return {String} the path to which we should redirect. |
62 | + */ |
63 | + popLoginRedirectPath: function() { |
64 | + var result = this.redirectPath; |
65 | + delete this.redirectPath; |
66 | + var currentPath = this.get('currentUrl'); |
67 | + var loginPath = /^\/login(\/|$)/; |
68 | + if (currentPath !== '/' && !loginPath.test(currentPath)) { |
69 | + // We used existing credentials or a token to go directly to a url. |
70 | + result = currentPath; |
71 | + } else if (!result || loginPath.test(result)) { |
72 | + result = '/'; |
73 | + } |
74 | + return result; |
75 | + }, |
76 | + |
77 | + /** |
78 | * Hide the login mask and redispatch the router. |
79 | * |
80 | * When the environment gets a response from a login attempt, |
81 | @@ -906,25 +960,31 @@ |
82 | */ |
83 | onLogin: function(e) { |
84 | if (e.data.result) { |
85 | - // We need to save the url to continue on to without redirecting |
86 | - // to root if there are extra path details. |
87 | + // The login was a success. |
88 | this.hideMask(); |
89 | - var originalPath = this.get('currentUrl'); |
90 | - if (originalPath !== '/' && !originalPath.match(/\/login\//)) { |
91 | - this.redirectPath = originalPath; |
92 | + var redirectPath = this.popLoginRedirectPath(); |
93 | + // Handle token authentication. |
94 | + if (e.data.fromToken) { |
95 | + // Alert the user. In the future, we might want to call out the |
96 | + // password so the user can note it. That will probably want a |
97 | + // modal or similar. |
98 | + this.env.onceAfter('environmentNameChange', function() { |
99 | + this.db.notifications.add( |
100 | + new models.Notification({ |
101 | + title: 'Logged in with Token', |
102 | + message: ('You have successfully logged in with a ' + |
103 | + 'single-use authentication token.'), |
104 | + level: 'important' |
105 | + }) |
106 | + ); |
107 | + }, this); |
108 | } |
109 | - if (originalPath.match(/login/) && this.redirectPath === '/') { |
110 | + if (redirectPath === '/') { |
111 | setTimeout( |
112 | Y.bind(this.showRootView, this), 0); |
113 | return; |
114 | } else { |
115 | - var nsRouter = this.nsRouter; |
116 | - |
117 | - this.navigate( |
118 | - nsRouter.url(nsRouter.parse(this.redirectPath)), |
119 | - {overrideAllNamespaces: true}); |
120 | - this.redirectPath = null; |
121 | - return; |
122 | + this.navigate(redirectPath, {overrideAllNamespaces: true}); |
123 | } |
124 | } else { |
125 | this.showLogin(); |
126 | @@ -1239,11 +1299,25 @@ |
127 | * @attribute currentUrl.getter |
128 | */ |
129 | getter: function() { |
130 | - return [ |
131 | - window.location.pathname, |
132 | - window.location.search, |
133 | - window.location.hash |
134 | - ].join(''); |
135 | + // The result is a normalized version of the currentURL. |
136 | + // Specifically, it omits any authtokens and uses our standard path |
137 | + // normalizing tool (currently the nsRouter). |
138 | + var nsRouter = this.nsRouter; |
139 | + // `this.location` is a test-friendly access of window.location. |
140 | + var routes = nsRouter.parse(this.location.toString()); |
141 | + if (routes.search) { |
142 | + var qs = Y.QueryString.parse(routes.search); |
143 | + var authtoken = qs.authtoken; |
144 | + if (Y.Lang.isValue(authtoken)) { |
145 | + // Remove the token from the URL. It is a one-shot, designed to |
146 | + // be consumed. We don't want it to be in the URL after it has |
147 | + // been used. |
148 | + delete qs.authtoken; |
149 | + routes.search = Y.QueryString.stringify(qs); |
150 | + } |
151 | + } |
152 | + // Use the nsRouter to normalize. |
153 | + return nsRouter.url(routes); |
154 | } |
155 | }, |
156 | /** |
157 | @@ -1358,6 +1432,7 @@ |
158 | 'model', |
159 | 'app-cookies-extension', |
160 | 'cookie', |
161 | + 'querystring', |
162 | 'app-subapp-extension', |
163 | 'sub-app', |
164 | 'subapp-browser', |
165 | |
166 | === modified file 'app/store/env/fakebackend.js' |
167 | --- app/store/env/fakebackend.js 2013-11-20 19:38:03 +0000 |
168 | +++ app/store/env/fakebackend.js 2013-11-26 22:03:26 +0000 |
169 | @@ -44,6 +44,7 @@ |
170 | FakeBackend.NAME = 'fake-backend'; |
171 | FakeBackend.ATTRS = { |
172 | authorizedUsers: {value: {'admin': 'password'}}, |
173 | + token: {value: 'demoToken'}, |
174 | authenticated: {value: false}, |
175 | store: {required: true}, |
176 | defaultSeries: {value: 'precise'}, |
177 | @@ -171,6 +172,26 @@ |
178 | return authenticated; |
179 | }, |
180 | |
181 | + |
182 | + /** |
183 | + Attempt to log a user in with a token. |
184 | + |
185 | + @method tokenlogin |
186 | + @param {String} submittedToken The authentication token. |
187 | + @return {Array} [username, password] if successful, or else undefined. |
188 | + */ |
189 | + tokenlogin: function(submittedToken) { |
190 | + var token = this.get('token'), |
191 | + authorizedUsers = this.get('authorizedUsers'), |
192 | + authenticated = token === submittedToken; |
193 | + this.set('authenticated', authenticated); |
194 | + if (authenticated) { |
195 | + var username = Object.keys(authorizedUsers)[0]; |
196 | + var password = authorizedUsers[username]; |
197 | + return [username, password]; |
198 | + } |
199 | + }, |
200 | + |
201 | /** |
202 | Log out. If already logged out, no error is raised. |
203 | @method logout |
204 | |
205 | === modified file 'app/store/env/go.js' |
206 | --- app/store/env/go.js 2013-11-20 18:45:37 +0000 |
207 | +++ app/store/env/go.js 2013-11-26 22:03:26 +0000 |
208 | @@ -317,21 +317,47 @@ |
209 | * |
210 | * @method handleLogin |
211 | * @param {Object} data The response returned by the server. |
212 | + * param {Bool} fromToken Whether the login request was via a token. |
213 | * @return {undefined} Nothing. |
214 | */ |
215 | - handleLogin: function(data) { |
216 | + handleLogin: function(data, fromToken) { |
217 | + fromToken = !!fromToken; // Normalize. |
218 | this.pendingLoginResponse = false; |
219 | this.userIsAuthenticated = !data.Error; |
220 | if (this.userIsAuthenticated) { |
221 | + // If this is a token login, set the credentials. |
222 | + var response = data.Response; |
223 | + if (response && response.AuthTag && response.Password) { |
224 | + this.setCredentials({ |
225 | + user: response.AuthTag, password: response.Password}); |
226 | + } |
227 | // If login succeeded retrieve the environment info. |
228 | this.environmentInfo(); |
229 | this._watchAll(); |
230 | + // Clean up for log out text. |
231 | + this.failedAuthentication = this.failedTokenAuthentication = false; |
232 | } else { |
233 | // If the credentials were rejected remove them. |
234 | this.setCredentials(null); |
235 | - this.failedAuthentication = true; |
236 | + // Indicate if the authentication was from a token. |
237 | + this.failedAuthentication = !fromToken; |
238 | + this.failedTokenAuthentication = fromToken; |
239 | } |
240 | - this.fire('login', {data: {result: this.userIsAuthenticated}}); |
241 | + this.fire( |
242 | + 'login', |
243 | + {data: {result: this.userIsAuthenticated, |
244 | + fromToken: fromToken}}); |
245 | + }, |
246 | + |
247 | + /** |
248 | + * React to the results of sending a token login message to the server. |
249 | + * |
250 | + * @method handleTokenLogin |
251 | + * @param {Object} data The response returned by the server. |
252 | + * @return {undefined} Nothing. |
253 | + */ |
254 | + handleTokenLogin: function(data) { |
255 | + this.handleLogin(data, true); |
256 | }, |
257 | |
258 | /** |
259 | @@ -368,6 +394,28 @@ |
260 | }, |
261 | |
262 | /** |
263 | + * Attempt to log the user in with a token. |
264 | + * |
265 | + * @method tokenLogin |
266 | + * @return {undefined} Nothing. |
267 | + */ |
268 | + tokenLogin: function(token) { |
269 | + // If the user is already authenticated there is nothing to do. |
270 | + if (this.userIsAuthenticated) { |
271 | + this.fire('login', {data: {result: true}}); |
272 | + return; |
273 | + } |
274 | + if (this.pendingLoginResponse) { |
275 | + return; |
276 | + } |
277 | + this._send_rpc({ |
278 | + Type: 'GUIToken', |
279 | + Request: 'Login', |
280 | + Params: {Token: token} |
281 | + }, this.handleTokenLogin); |
282 | + }, |
283 | + |
284 | + /** |
285 | * Store the environment info coming from the server. |
286 | * |
287 | * @method handleEnvironmentInfo |
288 | |
289 | === modified file 'app/store/env/sandbox.js' |
290 | --- app/store/env/sandbox.js 2013-11-20 19:42:15 +0000 |
291 | +++ app/store/env/sandbox.js 2013-11-26 22:03:26 +0000 |
292 | @@ -820,6 +820,32 @@ |
293 | }, |
294 | |
295 | /** |
296 | + Handle GUIToken Login messages to the state object. |
297 | + |
298 | + @method handleGUITokenLogin |
299 | + @param {Object} data The contents of the API arguments. |
300 | + @param {Object} client The active ClientConnection. |
301 | + @param {Object} state An instance of FakeBackend. |
302 | + @return {undefined} Side effects only. |
303 | + */ |
304 | + handleGUITokenLogin: function(data, client, state) { |
305 | + var response = state.tokenlogin(data.Params.Token); |
306 | + if (response) { |
307 | + client.receive({ |
308 | + RequestId: data.RequestId, |
309 | + Response: {AuthTag: response[0], Password: response[1]} |
310 | + }); |
311 | + } else { |
312 | + client.receive({ |
313 | + RequestId: data.RequestId, |
314 | + Error: 'unknown, fulfilled, or expired token', |
315 | + ErrorCode: 'unauthorized access', |
316 | + Response: {} |
317 | + }); |
318 | + } |
319 | + }, |
320 | + |
321 | + /** |
322 | Handle EnvironmentView messages. |
323 | |
324 | @method handleClientEnvironmentInfo |
325 | @@ -830,9 +856,12 @@ |
326 | */ |
327 | handleClientEnvironmentInfo: function(data, client, state) { |
328 | client.receive({ |
329 | - ProviderType: state.get('providerType'), |
330 | - DefaultSeries: state.get('defaultSeries'), |
331 | - Name: 'Sandbox' |
332 | + RequestId: data.RequestId, |
333 | + Response: { |
334 | + ProviderType: state.get('providerType'), |
335 | + DefaultSeries: state.get('defaultSeries'), |
336 | + Name: 'Sandbox' |
337 | + } |
338 | }); |
339 | }, |
340 | |
341 | |
342 | === modified file 'app/views/login.js' |
343 | --- app/views/login.js 2013-05-17 14:51:05 +0000 |
344 | +++ app/views/login.js 2013-11-26 22:03:26 +0000 |
345 | @@ -95,14 +95,20 @@ |
346 | environment_name_node.get('text') : 'Environment'); |
347 | var provider_type = ( |
348 | provider_type_node ? provider_type_node.get('text') : ''); |
349 | + var error_text = ''; |
350 | + if (env.failedAuthentication) { |
351 | + error_text = 'Unknown user or password.'; |
352 | + } else if (env.failedTokenAuthentication) { |
353 | + error_text = ( |
354 | + 'The one-time token was unknown, expired, or already used.'); |
355 | + } |
356 | // In order to have events work and the view cleanly be replaced by |
357 | // other views, we need to put the contents in the usual "container" |
358 | // node, even though it is not a child of the mask node. |
359 | this.get('container').setHTML(this.template({ |
360 | environment_name: environment_name, |
361 | provider_type: provider_type, |
362 | - error_text: ( |
363 | - env.failedAuthentication ? 'Unknown user or password.' : ''), |
364 | + error_text: error_text, |
365 | user: env.get('user') || env.defaultUser, |
366 | help_text: this.get('help_text') |
367 | })); |
368 | |
369 | === modified file 'test/test_app.js' |
370 | --- test/test_app.js 2013-11-05 18:10:05 +0000 |
371 | +++ test/test_app.js 2013-11-26 22:03:26 +0000 |
372 | @@ -346,6 +346,54 @@ |
373 | }); |
374 | }); |
375 | |
376 | + it('uses the authtoken when there are no credentials', function(done) { |
377 | + var app = makeApp(false); |
378 | + // Override the local window.location object. |
379 | + app.location = {search: '?authtoken=demoToken'}; |
380 | + env.setCredentials(null); |
381 | + env.connect(); |
382 | + app.after('ready', function() { |
383 | + assert.equal(conn.messages.length, 1); |
384 | + assert.deepEqual(conn.last_message(), { |
385 | + RequestId: 1, |
386 | + Type: 'GUIToken', |
387 | + Request: 'Login', |
388 | + Params: {Token: 'demoToken'} |
389 | + }); |
390 | + done(); |
391 | + }); |
392 | + }); |
393 | + |
394 | + it('handles multiple authtokens', function(done) { |
395 | + var app = makeApp(false); |
396 | + // Override the local window.location object. |
397 | + app.location = {search: '?authtoken=demoToken&authtoken=discarded'}; |
398 | + env.setCredentials(null); |
399 | + env.connect(); |
400 | + app.after('ready', function() { |
401 | + assert.equal(conn.messages.length, 1); |
402 | + assert.deepEqual(conn.last_message(), { |
403 | + RequestId: 1, |
404 | + Type: 'GUIToken', |
405 | + Request: 'Login', |
406 | + Params: {Token: 'demoToken'} |
407 | + }); |
408 | + done(); |
409 | + }); |
410 | + }); |
411 | + |
412 | + it('ignores the authtoken if credentials exist', function(done) { |
413 | + var app = makeApp(false); |
414 | + // Override the local window.location object. |
415 | + app.location = {search: '?authtoken=demoToken'}; |
416 | + env.connect(); |
417 | + app.after('ready', function() { |
418 | + assert.equal(1, conn.messages.length); |
419 | + assertIsLogin(conn.last_message()); |
420 | + done(); |
421 | + }); |
422 | + }); |
423 | + |
424 | it('displays the login view if credentials are not valid', function(done) { |
425 | var app = makeApp(true); // Create a connected app. |
426 | app.after('ready', function() { |
427 | @@ -360,12 +408,14 @@ |
428 | }); |
429 | |
430 | it('login method handler is called after successful login', function(done) { |
431 | - var oldOnLogin = Y.juju.App.onLogin; |
432 | + var oldOnLogin = Y.juju.App.prototype.onLogin; |
433 | Y.juju.App.prototype.onLogin = function(e) { |
434 | + // Clean up. |
435 | + Y.juju.App.prototype.onLogin = oldOnLogin; |
436 | + // Begin assertions. |
437 | assert.equal(conn.messages.length, 1); |
438 | assertIsLogin(conn.last_message()); |
439 | assert.isTrue(e.data.result, true); |
440 | - Y.juju.App.onLogin = oldOnLogin; |
441 | done(); |
442 | }; |
443 | var app = new Y.juju.App({ env: env, viewContainer: container }); |
444 | @@ -375,6 +425,50 @@ |
445 | app.destroy(true); |
446 | }); |
447 | |
448 | + it('creates a notification if logged in with a token', function(done) { |
449 | + // We need to change the prototype before we instantiate. |
450 | + // See the "this.reset()" call in the callback below that cleans up. |
451 | + var stub = utils.makeStubMethod(Y.juju.App.prototype, 'onLogin'); |
452 | + var app = makeApp(false); |
453 | + utils.makeStubMethod(app, 'hideMask'); |
454 | + app.redirectPath = '/foo/bar/'; |
455 | + app.location = { |
456 | + toString: function() {return '/login/';}, |
457 | + search: '?authtoken=demoToken'}; |
458 | + utils.makeStubMethod(app.env, 'onceAfter'); |
459 | + utils.makeStubMethod(app, 'navigate'); |
460 | + stub.addCallback(function() { |
461 | + // Clean up. |
462 | + this.reset(); |
463 | + // Begin assertions. |
464 | + var e = this.lastArguments()[0]; |
465 | + // These two really simply verify that our test prep did what we |
466 | + // expected. |
467 | + assert.isTrue(e.data.result); |
468 | + assert.isTrue(e.data.fromToken); |
469 | + this.passThroughToOriginalMethod(app); |
470 | + assert.isTrue(app.hideMask.calledOnce()); |
471 | + assert.isTrue(app.env.onceAfter.calledOnce()); |
472 | + var onceAfterArgs = app.env.onceAfter.lastArguments(); |
473 | + assert.equal(onceAfterArgs[0], 'environmentNameChange'); |
474 | + // Call the event handler so we can verify what it does. |
475 | + onceAfterArgs[1].call(onceAfterArgs[2]); |
476 | + assert.equal( |
477 | + app.db.notifications.item(0).get('title'), |
478 | + 'Logged in with Token'); |
479 | + assert.isTrue(app.navigate.calledOnce()); |
480 | + var navigateArgs = app.navigate.lastArguments(); |
481 | + assert.equal(navigateArgs[0], '/foo/bar/'); |
482 | + assert.deepEqual(navigateArgs[1], {overrideAllNamespaces: true}); |
483 | + done(); |
484 | + }); |
485 | + env.setCredentials(null); |
486 | + env.connect(); |
487 | + conn.msg({ |
488 | + RequestId: conn.last_message().RequestId, |
489 | + Response: {AuthTag: 'tokenuser', Password: 'tokenpasswd'}}); |
490 | + }); |
491 | + |
492 | it('tries to log in on first connection', function(done) { |
493 | // This is the case when credential are stashed. |
494 | var app = makeApp(true); // Create a disconnected app. |
495 | @@ -406,6 +500,96 @@ |
496 | assert.equal(null, env.getCredentials()); |
497 | }); |
498 | |
499 | + it('normally uses window.location', function() { |
500 | + // A lot of the app's authentication dance uses window.location, |
501 | + // both for redirects after login and for authtokens. For tests, |
502 | + // the app copies window.location to app.location, so that we |
503 | + // can easily override it. This test verifies that the initialization |
504 | + // actually does stash window.location as we exprect. |
505 | + var app = makeApp(false); |
506 | + assert.strictEqual(window.location, app.location); |
507 | + }); |
508 | + |
509 | + describe('popLoginRedirectPath', function() { |
510 | + it('returns and clears redirectPath', function() { |
511 | + var app = makeApp(false); |
512 | + app.redirectPath = '/foo/bar/'; |
513 | + app.location = {toString: function() {return '/login/';}}; |
514 | + assert.equal(app.popLoginRedirectPath(), '/foo/bar/'); |
515 | + assert.isUndefined(app.redirectPath); |
516 | + }); |
517 | + |
518 | + it('prefers the current path if not login', function() { |
519 | + var app = makeApp(false); |
520 | + app.redirectPath = '/'; |
521 | + app.location = {toString: function() {return '/foo/bar/';}}; |
522 | + assert.equal(app.popLoginRedirectPath(), '/foo/bar/'); |
523 | + assert.isUndefined(app.redirectPath); |
524 | + }); |
525 | + |
526 | + it('uses root if the redirectPath is /login/', function() { |
527 | + var app = makeApp(false); |
528 | + app.redirectPath = '/login/'; |
529 | + app.location = {toString: function() {return '/login/';}}; |
530 | + assert.equal(app.popLoginRedirectPath(), '/'); |
531 | + assert.isUndefined(app.redirectPath); |
532 | + }); |
533 | + |
534 | + it('uses root if the redirectPath is /login', function() { |
535 | + var app = makeApp(false); |
536 | + // Missing trailing slash is only difference from previous test. |
537 | + app.redirectPath = '/login'; |
538 | + app.location = {toString: function() {return '/login';}}; |
539 | + assert.equal(app.popLoginRedirectPath(), '/'); |
540 | + assert.isUndefined(app.redirectPath); |
541 | + }); |
542 | + }); |
543 | + |
544 | + describe('currentUrl', function() { |
545 | + it('returns the full current path', function() { |
546 | + var app = makeApp(false); |
547 | + var expected = '/foo/bar/'; |
548 | + app.location = { |
549 | + toString: function() {return 'https://foo.com' + expected;}}; |
550 | + assert.equal(expected, app.get('currentUrl')); |
551 | + expected = '/'; |
552 | + assert.equal(expected, app.get('currentUrl')); |
553 | + expected = '/foo/?bar=bing#shazam'; |
554 | + assert.equal(expected, app.get('currentUrl')); |
555 | + }); |
556 | + |
557 | + it('ignores authtokens', function() { |
558 | + // This is intended to be the canonical current path. This should |
559 | + // never include authtokens, which are transient and can never be |
560 | + // re-used. |
561 | + var app = makeApp(false); |
562 | + var expected_path = '/foo/bar/'; |
563 | + var expected_querystring = ''; |
564 | + var expected_hash = ''; |
565 | + var expected = function(add_authtoken) { |
566 | + var result = expected_path; |
567 | + var querystring = expected_querystring; |
568 | + if (add_authtoken) { |
569 | + querystring += '&authtoken=demoToken'; |
570 | + } |
571 | + if (querystring) { |
572 | + result += '?' + querystring; |
573 | + } |
574 | + result += expected_hash; |
575 | + return result; |
576 | + }; |
577 | + app.location = { |
578 | + toString: function() {return 'https://foo.com' + expected(true);}}; |
579 | + assert.equal(expected(), app.get('currentUrl')); |
580 | + expected_path = '/'; |
581 | + assert.equal(expected(), app.get('currentUrl')); |
582 | + expected_path = '/foo/'; |
583 | + expected_querystring = 'bar=bing'; |
584 | + expected_hash = '#shazam'; |
585 | + assert.equal(expected(), app.get('currentUrl')); |
586 | + }); |
587 | + }); |
588 | + |
589 | }); |
590 | })(); |
591 | |
592 | |
593 | === modified file 'test/test_env_go.js' |
594 | --- test/test_env_go.js 2013-11-20 18:45:37 +0000 |
595 | +++ test/test_env_go.js 2013-11-26 22:03:26 +0000 |
596 | @@ -82,7 +82,7 @@ |
597 | }); |
598 | |
599 | describe('Go Juju environment', function() { |
600 | - var conn, endpointA, endpointB, env, juju, msg, utils, Y, oldHandleLogin; |
601 | + var conn, endpointA, endpointB, env, juju, msg, utils, Y, cleanups; |
602 | |
603 | before(function(done) { |
604 | Y = YUI(GlobalConfig).use(['juju-env', 'juju-tests-utils'], function(Y) { |
605 | @@ -98,109 +98,220 @@ |
606 | conn: conn, user: 'user', password: 'password' |
607 | }, 'go'); |
608 | env.connect(); |
609 | + cleanups = []; |
610 | }); |
611 | |
612 | afterEach(function() { |
613 | - env.destroy(); |
614 | + cleanups.forEach(function(action) {action();}); |
615 | + // We need to clear any credentials stored in sessionStorage. |
616 | + env.setCredentials(null); |
617 | + if (env && env.destroy) {env.destroy();} |
618 | + if (conn && conn.destroy) {conn.destroy();} |
619 | }); |
620 | |
621 | var noopHandleLogin = function() { |
622 | - // In order to avoid rewriting all of these go tests we need to destroy |
623 | - // the env created in the beforeEach |
624 | - env.destroy(); |
625 | - // We need to clear any credentials stored in sessionStorage. |
626 | - env.setCredentials(null); |
627 | - oldHandleLogin = Y.juju.environments.GoEnvironment.handleLogin; |
628 | + var oldHandleLogin = Y.juju.environments.GoEnvironment.handleLogin; |
629 | Y.juju.environments.GoEnvironment.handleLogin = function() {}; |
630 | - conn = new utils.SocketStub(); |
631 | - env = juju.newEnvironment({ |
632 | - conn: conn, user: 'user', password: 'password' |
633 | - }, 'go'); |
634 | - env.connect(); |
635 | - }; |
636 | - |
637 | - var resetHandleLogin = function() { |
638 | - Y.juju.environments.GoEnvironment.handleLogin = oldHandleLogin; |
639 | - }; |
640 | - |
641 | - it('sends the correct login message', function() { |
642 | - noopHandleLogin(); |
643 | - env.login(); |
644 | - var last_message = conn.last_message(); |
645 | - var expected = { |
646 | - Type: 'Admin', |
647 | - Request: 'Login', |
648 | - RequestId: 1, |
649 | - Params: {AuthTag: 'user', Password: 'password'} |
650 | - }; |
651 | - assert.deepEqual(expected, last_message); |
652 | - resetHandleLogin(); |
653 | - }); |
654 | - |
655 | - it('resets the user and password if they are not valid', function() { |
656 | - env.login(); |
657 | - // Assume login to be the first request. |
658 | - conn.msg({RequestId: 1, Error: 'Invalid user or password'}); |
659 | - assert.isNull(env.getCredentials()); |
660 | - assert.isTrue(env.failedAuthentication); |
661 | - }); |
662 | - |
663 | - it('fires a login event on successful login', function() { |
664 | - var loginFired = false; |
665 | - var result; |
666 | - env.on('login', function(evt) { |
667 | - loginFired = true; |
668 | - result = evt.data.result; |
669 | - }); |
670 | - env.login(); |
671 | - // Assume login to be the first request. |
672 | - conn.msg({RequestId: 1, Response: {}}); |
673 | - assert.isTrue(loginFired); |
674 | - assert.isTrue(result); |
675 | - }); |
676 | - |
677 | - it('fires a login event on failed login', function() { |
678 | - var loginFired = false; |
679 | - var result; |
680 | - env.on('login', function(evt) { |
681 | - loginFired = true; |
682 | - result = evt.data.result; |
683 | - }); |
684 | - env.login(); |
685 | - // Assume login to be the first request. |
686 | - conn.msg({RequestId: 1, Error: 'Invalid user or password'}); |
687 | - assert.isTrue(loginFired); |
688 | - assert.isFalse(result); |
689 | - }); |
690 | - |
691 | - it('avoids sending login requests without credentials', function() { |
692 | - env.setCredentials(null); |
693 | - env.login(); |
694 | - assert.equal(0, conn.messages.length); |
695 | - }); |
696 | - |
697 | - it('calls environmentInfo and watchAll ofter login', function() { |
698 | - env.login(); |
699 | - // Assume login to be the first request. |
700 | - conn.msg({RequestId: 1, Response: {}}); |
701 | - var environmentInfoMessage = conn.last_message(2); |
702 | - // EnvironmentInfo is the second request. |
703 | - var environmentInfoExpected = { |
704 | - Type: 'Client', |
705 | - Request: 'EnvironmentInfo', |
706 | - RequestId: 2, |
707 | - Params: {} |
708 | - }; |
709 | - assert.deepEqual(environmentInfoExpected, environmentInfoMessage); |
710 | - var watchAllMessage = conn.last_message(); |
711 | - // EnvironmentInfo is the second request. |
712 | - var watchAllExpected = { |
713 | - Type: 'Client', |
714 | - Request: 'WatchAll', |
715 | - RequestId: 3, |
716 | - Params: {} |
717 | - }; |
718 | - assert.deepEqual(watchAllExpected, watchAllMessage); |
719 | + cleanups.push(function() { |
720 | + Y.juju.environments.GoEnvironment.handleLogin = oldHandleLogin; |
721 | + }); |
722 | + }; |
723 | + |
724 | + describe('login', function() { |
725 | + it('sends the correct login message', function() { |
726 | + noopHandleLogin(); |
727 | + env.login(); |
728 | + var last_message = conn.last_message(); |
729 | + var expected = { |
730 | + Type: 'Admin', |
731 | + Request: 'Login', |
732 | + RequestId: 1, |
733 | + Params: {AuthTag: 'user', Password: 'password'} |
734 | + }; |
735 | + assert.deepEqual(expected, last_message); |
736 | + }); |
737 | + |
738 | + it('resets the user and password if they are not valid', function() { |
739 | + env.login(); |
740 | + // Assume login to be the first request. |
741 | + conn.msg({RequestId: 1, Error: 'Invalid user or password'}); |
742 | + assert.isNull(env.getCredentials()); |
743 | + assert.isTrue(env.failedAuthentication); |
744 | + assert.isFalse(env.failedTokenAuthentication); |
745 | + }); |
746 | + |
747 | + it('fires a login event on successful login', function() { |
748 | + var loginFired = false; |
749 | + var result, fromToken; |
750 | + env.on('login', function(evt) { |
751 | + loginFired = true; |
752 | + result = evt.data.result; |
753 | + fromToken = evt.data.fromToken; |
754 | + }); |
755 | + env.login(); |
756 | + // Assume login to be the first request. |
757 | + conn.msg({RequestId: 1, Response: {}}); |
758 | + assert.isTrue(loginFired); |
759 | + assert.isTrue(result); |
760 | + assert.isFalse(fromToken); |
761 | + }); |
762 | + |
763 | + it('resets failed markers on successful login', function() { |
764 | + env.failedAuthentication = env.failedTokenAuthentication = true; |
765 | + env.login(); |
766 | + // Assume login to be the first request. |
767 | + conn.msg({RequestId: 1, Response: {}}); |
768 | + assert.isFalse(env.failedAuthentication); |
769 | + assert.isFalse(env.failedTokenAuthentication); |
770 | + }); |
771 | + |
772 | + it('fires a login event on failed login', function() { |
773 | + var loginFired = false; |
774 | + var result; |
775 | + env.on('login', function(evt) { |
776 | + loginFired = true; |
777 | + result = evt.data.result; |
778 | + }); |
779 | + env.login(); |
780 | + // Assume login to be the first request. |
781 | + conn.msg({RequestId: 1, Error: 'Invalid user or password'}); |
782 | + assert.isTrue(loginFired); |
783 | + assert.isFalse(result); |
784 | + }); |
785 | + |
786 | + it('avoids sending login requests without credentials', function() { |
787 | + env.setCredentials(null); |
788 | + env.login(); |
789 | + assert.equal(0, conn.messages.length); |
790 | + }); |
791 | + |
792 | + it('calls environmentInfo and watchAll ofter login', function() { |
793 | + env.login(); |
794 | + // Assume login to be the first request. |
795 | + conn.msg({RequestId: 1, Response: {}}); |
796 | + var environmentInfoMessage = conn.last_message(2); |
797 | + // EnvironmentInfo is the second request. |
798 | + var environmentInfoExpected = { |
799 | + Type: 'Client', |
800 | + Request: 'EnvironmentInfo', |
801 | + RequestId: 2, |
802 | + Params: {} |
803 | + }; |
804 | + assert.deepEqual(environmentInfoExpected, environmentInfoMessage); |
805 | + var watchAllMessage = conn.last_message(); |
806 | + // EnvironmentInfo is the second request. |
807 | + var watchAllExpected = { |
808 | + Type: 'Client', |
809 | + Request: 'WatchAll', |
810 | + RequestId: 3, |
811 | + Params: {} |
812 | + }; |
813 | + assert.deepEqual(watchAllExpected, watchAllMessage); |
814 | + }); |
815 | + }); |
816 | + |
817 | + describe('tokenLogin', function() { |
818 | + it('sends the correct tokenLogin message', function() { |
819 | + noopHandleLogin(); |
820 | + env.tokenLogin('demoToken'); |
821 | + var last_message = conn.last_message(); |
822 | + var expected = { |
823 | + Type: 'GUIToken', |
824 | + Request: 'Login', |
825 | + RequestId: 1, |
826 | + Params: {Token: 'demoToken'} |
827 | + }; |
828 | + assert.deepEqual(expected, last_message); |
829 | + }); |
830 | + |
831 | + it('resets the user and password if the token is not valid', function() { |
832 | + env.tokenLogin('badToken'); |
833 | + // Assume login to be the first request. |
834 | + conn.msg({ |
835 | + RequestId: 1, |
836 | + Error: 'unknown, fulfilled, or expired token', |
837 | + ErrorCode: 'unauthorized access' |
838 | + }); |
839 | + assert.isNull(env.getCredentials()); |
840 | + assert.isTrue(env.failedTokenAuthentication); |
841 | + assert.isFalse(env.failedAuthentication); |
842 | + }); |
843 | + |
844 | + it('fires a login event on successful token login', function() { |
845 | + var loginFired = false; |
846 | + var result, fromToken; |
847 | + env.on('login', function(evt) { |
848 | + loginFired = true; |
849 | + result = evt.data.result; |
850 | + fromToken = evt.data.fromToken; |
851 | + }); |
852 | + env.tokenLogin('demoToken'); |
853 | + // Assume login to be the first request. |
854 | + conn.msg({ |
855 | + RequestId: 1, |
856 | + Response: {AuthTag: 'tokenuser', Password: 'tokenpasswd'}}); |
857 | + assert.isTrue(loginFired); |
858 | + assert.isTrue(result); |
859 | + assert.isTrue(fromToken); |
860 | + var credentials = env.getCredentials(); |
861 | + assert.equal('tokenuser', credentials.user); |
862 | + assert.equal('tokenpasswd', credentials.password); |
863 | + }); |
864 | + |
865 | + it('resets failed markers on successful login', function() { |
866 | + env.failedAuthentication = env.failedTokenAuthentication = true; |
867 | + env.tokenLogin('demoToken'); |
868 | + // Assume login to be the first request. |
869 | + conn.msg({ |
870 | + RequestId: 1, |
871 | + Response: {AuthTag: 'tokenuser', Password: 'tokenpasswd'}}); |
872 | + assert.isFalse(env.failedAuthentication); |
873 | + assert.isFalse(env.failedTokenAuthentication); |
874 | + }); |
875 | + |
876 | + it('fires a login event on failed token login', function() { |
877 | + var loginFired = false; |
878 | + var result; |
879 | + env.on('login', function(evt) { |
880 | + loginFired = true; |
881 | + result = evt.data.result; |
882 | + }); |
883 | + env.tokenLogin('badToken'); |
884 | + // Assume login to be the first request. |
885 | + conn.msg({ |
886 | + RequestId: 1, |
887 | + Error: 'unknown, fulfilled, or expired token', |
888 | + ErrorCode: 'unauthorized access' |
889 | + }); |
890 | + assert.isTrue(loginFired); |
891 | + assert.isFalse(result); |
892 | + }); |
893 | + |
894 | + it('calls environmentInfo and watchAll ofter token login', function() { |
895 | + env.tokenLogin('demoToken'); |
896 | + // Assume login to be the first request. |
897 | + conn.msg({ |
898 | + RequestId: 1, |
899 | + Response: {AuthTag: 'tokenuser', Password: 'tokenpasswd'}}); |
900 | + var environmentInfoMessage = conn.last_message(2); |
901 | + // EnvironmentInfo is the second request. |
902 | + var environmentInfoExpected = { |
903 | + Type: 'Client', |
904 | + Request: 'EnvironmentInfo', |
905 | + RequestId: 2, |
906 | + Params: {} |
907 | + }; |
908 | + assert.deepEqual(environmentInfoExpected, environmentInfoMessage); |
909 | + var watchAllMessage = conn.last_message(); |
910 | + // EnvironmentInfo is the second request. |
911 | + var watchAllExpected = { |
912 | + Type: 'Client', |
913 | + Request: 'WatchAll', |
914 | + RequestId: 3, |
915 | + Params: {} |
916 | + }; |
917 | + assert.deepEqual(watchAllExpected, watchAllMessage); |
918 | + }); |
919 | }); |
920 | |
921 | it('sends the correct request for environment info', function() { |
922 | |
923 | === modified file 'test/test_fakebackend.js' |
924 | --- test/test_fakebackend.js 2013-11-20 19:38:03 +0000 |
925 | +++ test/test_fakebackend.js 2013-11-26 22:03:26 +0000 |
926 | @@ -60,6 +60,37 @@ |
927 | }); |
928 | }); |
929 | |
930 | + describe('FakeBackend.tokenlogin', function() { |
931 | + var requires = ['node', 'juju-env-fakebackend']; |
932 | + var Y, environmentsModule, fakebackend; |
933 | + |
934 | + before(function(done) { |
935 | + Y = YUI(GlobalConfig).use(requires, function(Y) { |
936 | + environmentsModule = Y.namespace('juju.environments'); |
937 | + done(); |
938 | + }); |
939 | + }); |
940 | + |
941 | + afterEach(function() { |
942 | + fakebackend.destroy(); |
943 | + }); |
944 | + |
945 | + it('authenticates', function() { |
946 | + fakebackend = new environmentsModule.FakeBackend(); |
947 | + assert.equal(fakebackend.get('authenticated'), false); |
948 | + assert.deepEqual( |
949 | + fakebackend.tokenlogin('demoToken'), ['admin', 'password']); |
950 | + assert.equal(fakebackend.get('authenticated'), true); |
951 | + }); |
952 | + |
953 | + it('refuses to authenticate', function() { |
954 | + fakebackend = new environmentsModule.FakeBackend(); |
955 | + assert.equal(fakebackend.get('authenticated'), false); |
956 | + assert.isUndefined(fakebackend.tokenlogin('not the token')); |
957 | + assert.equal(fakebackend.get('authenticated'), false); |
958 | + }); |
959 | + }); |
960 | + |
961 | describe('FakeBackend.deploy', function() { |
962 | var requires = [ |
963 | 'node', 'juju-tests-utils', 'juju-models', 'juju-charm-models']; |
964 | |
965 | === modified file 'test/test_login.js' |
966 | --- test/test_login.js 2013-08-27 16:24:00 +0000 |
967 | +++ test/test_login.js 2013-11-26 22:03:26 +0000 |
968 | @@ -44,6 +44,7 @@ |
969 | env.destroy(); |
970 | }); |
971 | |
972 | + // These duplicate more thorough tests in test_env_go.js. |
973 | test('the user is initially assumed to be unauthenticated', function() { |
974 | assert.isFalse(env.userIsAuthenticated); |
975 | }); |
976 | |
977 | === modified file 'test/test_sandbox_go.js' |
978 | --- test/test_sandbox_go.js 2013-11-20 18:45:37 +0000 |
979 | +++ test/test_sandbox_go.js 2013-11-26 22:03:26 +0000 |
980 | @@ -45,6 +45,8 @@ |
981 | }); |
982 | |
983 | afterEach(function() { |
984 | + // We need to clear any credentials stored in sessionStorage. |
985 | + env.setCredentials(null); |
986 | env.destroy(); |
987 | client.destroy(); |
988 | juju.destroy(); |
989 | @@ -154,6 +156,91 @@ |
990 | env.login(); |
991 | }); |
992 | |
993 | + it('can log in with a token.', function(done) { |
994 | + // See FakeBackend's initialization for these default authentication |
995 | + // values. |
996 | + var data = { |
997 | + Type: 'GUIToken', |
998 | + Request: 'Login', |
999 | + Params: { |
1000 | + Token: 'demoToken' |
1001 | + }, |
1002 | + RequestId: 42 |
1003 | + }; |
1004 | + client.onmessage = function(received) { |
1005 | + var expected = { |
1006 | + RequestId: 42, Response: {AuthTag: 'admin', Password: 'password'}}; |
1007 | + assert.deepEqual(Y.JSON.parse(received.data), expected); |
1008 | + assert.isTrue(state.get('authenticated')); |
1009 | + done(); |
1010 | + }; |
1011 | + state.logout(); |
1012 | + assert.isFalse(state.get('authenticated')); |
1013 | + client.open(); |
1014 | + client.send(Y.JSON.stringify(data)); |
1015 | + }); |
1016 | + |
1017 | + it('does not log in with a bad token.', function(done) { |
1018 | + // See FakeBackend's initialization for these default authentication |
1019 | + // values. |
1020 | + var data = { |
1021 | + Type: 'GUIToken', |
1022 | + Request: 'Login', |
1023 | + Params: { |
1024 | + Token: 'badToken' |
1025 | + }, |
1026 | + RequestId: 42 |
1027 | + }; |
1028 | + client.onmessage = function(received) { |
1029 | + var expected = { |
1030 | + RequestId: 42, |
1031 | + Error: 'unknown, fulfilled, or expired token', |
1032 | + ErrorCode: 'unauthorized access', |
1033 | + Response: {}}; |
1034 | + assert.deepEqual(Y.JSON.parse(received.data), expected); |
1035 | + assert.isFalse(state.get('authenticated')); |
1036 | + done(); |
1037 | + }; |
1038 | + state.logout(); |
1039 | + assert.isFalse(state.get('authenticated')); |
1040 | + client.open(); |
1041 | + client.send(Y.JSON.stringify(data)); |
1042 | + }); |
1043 | + |
1044 | + it('can log in with a token (environment integration).', function(done) { |
1045 | + state.logout(); |
1046 | + env.after('login', function() { |
1047 | + assert.isTrue(env.userIsAuthenticated); |
1048 | + assert.deepEqual(env.getCredentials(), |
1049 | + {user: 'admin', password: 'password'}); |
1050 | + done(); |
1051 | + }); |
1052 | + env.connect(); |
1053 | + assert.isFalse(env.getCredentials().areAvailable); |
1054 | + env.tokenLogin('demoToken'); |
1055 | + }); |
1056 | + |
1057 | + it('can return environment information.', function(done) { |
1058 | + // See FakeBackend's initialization for these default values. |
1059 | + var data = { |
1060 | + Type: 'Client', |
1061 | + Request: 'EnvironmentInfo', |
1062 | + RequestId: 42 |
1063 | + }; |
1064 | + client.onmessage = function(received) { |
1065 | + var expected = { |
1066 | + RequestId: 42, |
1067 | + Response: { |
1068 | + ProviderType: state.get('providerType'), |
1069 | + DefaultSeries: state.get('defaultSeries'), |
1070 | + Name: 'Sandbox'}}; |
1071 | + assert.deepEqual(Y.JSON.parse(received.data), expected); |
1072 | + done(); |
1073 | + }; |
1074 | + client.open(); |
1075 | + client.send(Y.JSON.stringify(data)); |
1076 | + }); |
1077 | + |
1078 | it('can start the AllWatcher', function(done) { |
1079 | var data = { |
1080 | Type: 'Client', |
1081 | |
1082 | === modified file 'test/utils.js' |
1083 | --- test/utils.js 2013-11-21 15:04:59 +0000 |
1084 | +++ test/utils.js 2013-11-26 22:03:26 +0000 |
1085 | @@ -24,6 +24,77 @@ |
1086 | |
1087 | jujuTests.utils = { |
1088 | |
1089 | + /** |
1090 | + * Make a stub function. Pass in 0 or more arguments to become responses |
1091 | + * that the function cycles through. |
1092 | + * |
1093 | + * @method makeStubFunction |
1094 | + * @return {Function} the new stub function. |
1095 | + */ |
1096 | + makeStubFunction: function() { |
1097 | + var responses = Array.prototype.slice.call(arguments, 0); |
1098 | + if (responses.length === 0) { |
1099 | + responses.push(undefined); |
1100 | + } |
1101 | + var f = function() { |
1102 | + var response = responses[(f._allArguments.length) % responses.length]; |
1103 | + f._allArguments.push(Array.prototype.slice.call(arguments, 0)); |
1104 | + f._callbacks.forEach(function(cb) {cb.call(f);}); |
1105 | + return response; |
1106 | + }; |
1107 | + f._allArguments = []; |
1108 | + f._callbacks = []; |
1109 | + f.called = function() { |
1110 | + return !!f._allArguments.length; |
1111 | + }; |
1112 | + f.calledOnce = function() { |
1113 | + return f._allArguments.length === 1; |
1114 | + }; |
1115 | + f.callCount = function() { |
1116 | + return f._allArguments.length; |
1117 | + }; |
1118 | + f.lastArguments = function() { |
1119 | + return f._allArguments[f._allArguments.length - 1]; |
1120 | + }; |
1121 | + f.allArguments = function() { |
1122 | + return f._allArguments.slice(0); |
1123 | + }; |
1124 | + f.addCallback = function(cb) { |
1125 | + f._callbacks.push(cb); |
1126 | + }; |
1127 | + return f; |
1128 | + }, |
1129 | + |
1130 | + /** |
1131 | + * Make a stub method. Pass in 0 or more arguments to become responses |
1132 | + * that the method cycles through. |
1133 | + * |
1134 | + * The function has all introspection methods from makeMockFunction, |
1135 | + * plus "reset", which resets the object with the original value. |
1136 | + * This is pre-bound, so it is easy to pass in as a clean-up function. |
1137 | + * |
1138 | + * @method makeStubMethod |
1139 | + * @param {Object} context the object on which the method will sit. |
1140 | + * @param {String} name the name to be replaced. |
1141 | + * @return {Function} the new stub function. |
1142 | + */ |
1143 | + makeStubMethod: function(context, name) { |
1144 | + var responses = Array.prototype.slice.call(arguments, 2); |
1145 | + var original = context[name]; |
1146 | + var f = context[name] = jujuTests.utils.makeStubFunction.apply( |
1147 | + jujuTests.utils, responses); |
1148 | + f.reset = function() { |
1149 | + context[name] = original; |
1150 | + }; |
1151 | + f.passThroughToOriginalMethod = function(instance) { |
1152 | + if (!Y.Lang.isValue(instance)) { |
1153 | + instance = context; |
1154 | + } |
1155 | + return original.apply(instance, f.lastArguments()); |
1156 | + }; |
1157 | + return f; |
1158 | + }, |
1159 | + |
1160 | makeContainer: function(id, visibleContainer) { |
1161 | var container = Y.Node.create('<div>'); |
1162 | if (id) { |
Reviewers: mp+196782_ code.launchpad. net,
Message:
Please take a look.
Description:
Add authtoken support to the GUI
The authtoken support in this branch includes support for both a real
environment and the sandbox. In writing this and testing it, I
encountered some code that was not working, and some code that was not
tested, and some code that was very difficult to test. This branch is
utterly gigantic, for which I apologize. It includes the changes I
needed to get everything else working. I should factor it out, now that
I have figured out what needs to be done, but I'm a bit fatigued, so I'm
asking for reviewer indulgence.
There are two ways you should QA. First, in the sandbox, delete the localhost: 8888?authtoken= demoToken . This should log you in, demoToken in demoToken# bws-whatever is correct, not the other way around.* badToken. It should send you to the login page with an
user and password from the config-debug file and then load the GUI in
your browser using a URL like this:
http://
remove the authtoken from the URL, and notify you that you used a token
to authenticate. Find some other URLs, like in the charm browser, and
copy them. Log out with the button on the top right. Paste the
previous URL in to the browser, and then insert ?authtoken=
the URL. *Note that a querystring should come before a hash, so
?authtoken=
Maybe do that log in, log out cycle a couple of times to try a few
different URLs. Now log out and try a different token, like
?authtoken=
appropriate error message.
Now it's time to QA a live environment. Here's how I suggest you do it.
1. In this branch, run BRANCH_IS_GOOD=1 make distfile . When it is
finished, it will tell you what file it made.
2. Get a copy of the lp:~juju-gui/charms/precise/juju-gui/trunk/ branch
if you don't have one already. If you do have one, make sure it is up
to date.
3. mv the file you made in step 1 to the charm's releases directory. rm
the old release in that directory.
4. juju bootstrap.
5. In the charm, run make deploy. Wait until it says it is done
deploying the code.
6. Start up Python in your local machine. Edit the following code to environments. yaml file.
include the address from step 5 and the appropriate password from your
~/.juju/
import itertools 21-197- 193.compute- 1.amazonaws. com {}:443/ ws'.format( address) create_ connection( url) 'RequestId' ] = counter.next() send(json. dumps(request) ) pprint( json.loads( ws.recv( )))
import json
import pprint
import websocket
address = 'PUBLICADDRESS' # e.g.
ec2-107-
password = 'YOURPASSWORD'
url = 'wss://
ws = websocket.
counter = itertools.count()
def process(request):
request = request.copy()
request[
ws.
pprint.
process( dict(Type= 'Admin' , Request='Login', Params={'AuthTag': dict(Type= 'GUIToken' , Request='Create', Params={}))
'user-admin', 'Password': password}))
process(
The last response should be something like this:
{u'RequestId': 2, 11-25T20: 11:41.624417Z' ,
u'Expires' : u'2013- 11-25T20: 13:41.624417Z' ,
u'Token' : u'e8ea8ac912fc4 ef6a355e82bb65c af6d'}}
u'Response': {u'Created': u'2013-
7. Now in your browser construct a url that has the GUI address from
step 5 and the ...