Merge lp:~benji/juju-gui/charmworld-api-3 into lp:juju-gui/experimental
- charmworld-api-3
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1056 |
Proposed branch: | lp:~benji/juju-gui/charmworld-api-3 |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
671 lines (+487/-53) 3 files modified
app/app.js (+5/-1) app/store/charm.js (+196/-34) test/test_charm_store.js (+286/-18) |
To merge this branch: | bzr merge lp:~benji/juju-gui/charmworld-api-3 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Richard Harding | Approve | ||
Review via email: mp+185325@code.launchpad.net |
Commit message
Description of the change
Add charmworld v3 API support.
The v2 API was changed to be a subclass of v3 so that removing v2 when it is
retired will be easy. There is a feature flag to enable the v3 API.
Benji York (benji) wrote : | # |
*** Submitted:
More/tweaked tests for the charmworld v2 API
The code was also tweaked in directions suggested by the new/refactored tests.
Gary Poster (gary) wrote : | # |
Hey. I *think* this LGTM but would like some clarifications. This
would have been a great branch for some pre-review comments, at least
for me.
https:/
File app/store/charm.js (left):
https:/
app/store/
Don't we still want this for APIv2?
https:/
app/store/
why don't we need this now? was it never used? I don't see it in your
APIv2 so...I'm confused.
https:/
File app/store/charm.js (right):
https:/
app/store/
I'm assuming this has no changes?
Gary Poster (gary) wrote : | # |
Cool, thanks for the explanations. It would have been nice to also see
the new tests, but anyway, LGTM, fly away. :-)
https:/
File app/store/charm.js (right):
https:/
app/store/
My main comment, in line with your reply above, is that explaining the
situation would be nice. Beyond that, Maybe @extends is supposed to say
"{APIv3}"? <shrug>
Preview Diff
1 | === modified file 'app/app.js' |
2 | --- app/app.js 2013-09-13 19:52:55 +0000 |
3 | +++ app/app.js 2013-09-17 19:00:33 +0000 |
4 | @@ -1379,7 +1379,11 @@ |
5 | } else { |
6 | cfg.apiHost = window.juju_config.charmworldURL; |
7 | } |
8 | - return new Y.juju.charmworld.APIv2(cfg); |
9 | + if (window.flags.charmworldv3) { |
10 | + return new Y.juju.charmworld.APIv3(cfg); |
11 | + } else { |
12 | + return new Y.juju.charmworld.APIv2(cfg); |
13 | + } |
14 | } |
15 | }, |
16 | |
17 | |
18 | === modified file 'app/store/charm.js' |
19 | --- app/store/charm.js 2013-09-17 17:56:07 +0000 |
20 | +++ app/store/charm.js 2013-09-17 19:00:33 +0000 |
21 | @@ -109,10 +109,6 @@ |
22 | } |
23 | result = defaultSeries + '/' + result; |
24 | } |
25 | - if (/\-(\d+|HEAD)/.exec(result) === null) { |
26 | - // Add in a revision placeholder |
27 | - result = result + '-1'; |
28 | - } |
29 | return result; |
30 | } |
31 | |
32 | @@ -122,14 +118,14 @@ |
33 | }); |
34 | |
35 | /** |
36 | - * Api helper for the charmworld API v2. |
37 | + * Charmworld API version 3 interface. |
38 | * |
39 | - * @class APIv2 |
40 | + * @class APIv3 |
41 | * @extends {Base} |
42 | * |
43 | */ |
44 | - ns.APIv2 = Y.Base.create('APIv2', Y.Base, [], { |
45 | - _apiRoot: 'api/2', |
46 | + ns.APIv3 = Y.Base.create('APIv3', Y.Base, [], { |
47 | + _apiRoot: 'api/3', |
48 | |
49 | /** |
50 | * Send the actual request and handle response from the api. |
51 | @@ -159,7 +155,7 @@ |
52 | * @param {Object} bindScope the scope of *this* in the callbacks. |
53 | */ |
54 | autocomplete: function(filters, callbacks, bindScope) { |
55 | - var endpoint = 'charms'; |
56 | + var endpoint = 'search'; |
57 | // Force that this is an autocomplete call to perform matching on the |
58 | // start of names vs a fulltext search. |
59 | filters.autocomplete = 'true'; |
60 | @@ -263,8 +259,8 @@ |
61 | @return {Promise} A promise for a newer charm ID or undefined. |
62 | */ |
63 | promiseUpgradeAvailability: function(charm, cache) { |
64 | - // Get the charm's store ID, then replace the version number |
65 | - // with '-HEAD' to retrieve the latest version of the charm. |
66 | + // Get the charm's store ID, then remove the version number to retrieve |
67 | + // the latest version of the charm. |
68 | var storeId, revision; |
69 | if (charm instanceof Y.Model) { |
70 | storeId = charm.get('storeId'); |
71 | @@ -273,7 +269,7 @@ |
72 | storeId = charm.url; |
73 | revision = parseInt(charm.revision, 10); |
74 | } |
75 | - storeId = storeId.replace(/-\d+$/, '-HEAD'); |
76 | + storeId = storeId.replace(/-\d+$/, ''); |
77 | // XXX By using a cache we hide charm versions that have become available |
78 | // since we last requested the most recent version. |
79 | return this.promiseCharm(storeId, cache) |
80 | @@ -296,7 +292,7 @@ |
81 | * @param {Object} bindScope the scope of *this* in the callbacks. |
82 | */ |
83 | search: function(filters, callbacks, bindScope) { |
84 | - var endpoint = 'charms'; |
85 | + var endpoint = 'search'; |
86 | if (bindScope) { |
87 | callbacks.success = Y.bind(callbacks.success, bindScope); |
88 | callbacks.failure = Y.bind(callbacks.failure, bindScope); |
89 | @@ -346,25 +342,6 @@ |
90 | }, |
91 | |
92 | /** |
93 | - Generate the API path to a file. |
94 | - This is useful when generating links and references in HTML to a file |
95 | - but not actually fetching the file itself. |
96 | - |
97 | - @method filepath |
98 | - @param {String} charmID The id of the charm to grab the file from. |
99 | - @param {String} filename The name of the file to generate a path to. |
100 | - |
101 | - */ |
102 | - filepath: function(charmID, filename) { |
103 | - return this.get('apiHost') + [ |
104 | - this._apiRoot, |
105 | - 'charm', |
106 | - charmID, |
107 | - 'file', |
108 | - filename].join('/'); |
109 | - }, |
110 | - |
111 | - /** |
112 | Generate the API path to a charm icon. |
113 | This is useful when generating links and references in HTML to the |
114 | charm's icon and is constructing the correct icon based on reviewed |
115 | @@ -390,11 +367,11 @@ |
116 | // The following regular expression removes everything up to the |
117 | // colon portion of the quote and leaves behind a charm ID. |
118 | charmID = charmID.replace(/^[^:]+:/, ''); |
119 | - |
120 | return this.get('apiHost') + [ |
121 | this._apiRoot, |
122 | 'charm', |
123 | charmID, |
124 | + 'file', |
125 | 'icon.svg'].join('/'); |
126 | } |
127 | }, |
128 | @@ -487,7 +464,7 @@ |
129 | callbacks.failure = Y.bind(callbacks.failure, bindScope); |
130 | } |
131 | |
132 | - this._makeRequest('charms/interesting', callbacks); |
133 | + this._makeRequest('search/interesting', callbacks); |
134 | }, |
135 | |
136 | /** |
137 | @@ -565,6 +542,191 @@ |
138 | } |
139 | }); |
140 | |
141 | + /** |
142 | + * Charmworld API version 2 interface. |
143 | + * |
144 | + * @class APIv2 |
145 | + * @extends {Base} |
146 | + * |
147 | + */ |
148 | + ns.APIv2 = Y.Base.create('APIv2', ns.APIv3, [], { |
149 | + _apiRoot: 'api/2', |
150 | + |
151 | + /** |
152 | + * Api call to fetch autocomplete suggestions based on the current term. |
153 | + * |
154 | + * @method autocomplete |
155 | + * @param {Object} query the filters data object for search. |
156 | + * @param {Object} filters the filters data object for search. |
157 | + * @param {Object} callbacks the success/failure callbacks to use. |
158 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
159 | + */ |
160 | + autocomplete: function(filters, callbacks, bindScope) { |
161 | + var endpoint = 'charms'; |
162 | + // Force that this is an autocomplete call to perform matching on the |
163 | + // start of names vs a fulltext search. |
164 | + filters.autocomplete = 'true'; |
165 | + filters.limit = 5; |
166 | + if (bindScope) { |
167 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
168 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
169 | + } |
170 | + this._makeRequest(endpoint, callbacks, filters); |
171 | + }, |
172 | + |
173 | + /** |
174 | + * Api call to fetch a charm's details, with an optional local cache. |
175 | + * |
176 | + * @method charmWithCache |
177 | + * @param {String} charmID The charm to fetch This is the fully qualified |
178 | + * charm name in the format scheme:series/charm-revision. |
179 | + * @param {Object} callbacks The success/failure callbacks to use. |
180 | + * @param {Object} bindScope The scope of "this" in the callbacks. |
181 | + * @param {ModelList} [cache] a local cache of browser charms. |
182 | + * @param {String} [defaultSeries='precise'] The series to use if none is |
183 | + * specified in the charm ID. |
184 | + */ |
185 | + charm: function(charmID, callbacks, bindScope, cache, defaultSeries) { |
186 | + if (bindScope) { |
187 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
188 | + } |
189 | + if (cache) { |
190 | + var charm = cache.getById(charmID); |
191 | + if (charm) { |
192 | + // If the charm was found in the cache, then we can declare success |
193 | + // without ever making a request to charmworld. |
194 | + Y.soon(function() { |
195 | + // Since there wasn't really a request, there is no data, so we |
196 | + // pass an empty object as the "data" parameter. |
197 | + callbacks.success({}, charm); |
198 | + }); |
199 | + return; |
200 | + } else { |
201 | + var successCB = callbacks.success; |
202 | + callbacks.success = function(data) { |
203 | + var charm = new Y.juju.models.Charm(data.charm); |
204 | + if (data.metadata) { |
205 | + charm.set('metadata', data.metadata); |
206 | + } |
207 | + cache.add(charm); |
208 | + successCB(data, charm); |
209 | + }; |
210 | + } |
211 | + } |
212 | + charmID = this.apiHelper.normalizeCharmId(charmID, defaultSeries); |
213 | + // If the charm ID does not have a revision number (or "HEAD"), add one. |
214 | + if (/\-(\d+|HEAD)/.exec(charmID) === null) { |
215 | + // Add in a revision placeholder. Any value will do, v2 of the |
216 | + // charmworld API ignores revision numbers. |
217 | + charmID = charmID + '-1'; |
218 | + } |
219 | + this._charm(charmID, callbacks, bindScope); |
220 | + }, |
221 | + |
222 | + /** |
223 | + Promises to return the latest charm ID for a given charm if a newer one |
224 | + exists; this also caches the newer charm if one is available. |
225 | + |
226 | + @method promiseUpgradeAvailability |
227 | + @param {Charm} charm An existing charm potentially in need of an upgrade. |
228 | + @param {ModelList} cache A local cache of browser charms. |
229 | + @return {Promise} A promise for a newer charm ID or undefined. |
230 | + */ |
231 | + promiseUpgradeAvailability: function(charm, cache) { |
232 | + // Get the charm's store ID, then replace the version number |
233 | + // with '-HEAD' to retrieve the latest version of the charm. |
234 | + var storeId, revision; |
235 | + if (charm instanceof Y.Model) { |
236 | + storeId = charm.get('storeId'); |
237 | + revision = parseInt(charm.get('revision'), 10); |
238 | + } else { |
239 | + storeId = charm.url; |
240 | + revision = parseInt(charm.revision, 10); |
241 | + } |
242 | + storeId = storeId.replace(/-\d+$/, '-HEAD'); |
243 | + // XXX By using a cache we hide charm versions that have become available |
244 | + // since we last requested the most recent version. |
245 | + return this.promiseCharm(storeId, cache) |
246 | + .then(function(latest) { |
247 | + var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10); |
248 | + if (latestVersion > revision) { |
249 | + return latest.charm.id; |
250 | + } |
251 | + }, function(e) { |
252 | + throw e; |
253 | + }); |
254 | + }, |
255 | + |
256 | + /** |
257 | + * Api call to search charms |
258 | + * |
259 | + * @method search |
260 | + * @param {Object} filters the filters data object for search. |
261 | + * @param {Object} callbacks the success/failure callbacks to use. |
262 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
263 | + */ |
264 | + search: function(filters, callbacks, bindScope) { |
265 | + var endpoint = 'charms'; |
266 | + if (bindScope) { |
267 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
268 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
269 | + } |
270 | + this._makeRequest(endpoint, callbacks, filters); |
271 | + }, |
272 | + |
273 | + /** |
274 | + Generate the API path to a charm icon. |
275 | + This is useful when generating links and references in HTML to the |
276 | + charm's icon and is constructing the correct icon based on reviewed |
277 | + status and categories on the charm. |
278 | + |
279 | + @method iconpath |
280 | + @param {String} charmID The id of the charm to grab the icon for. |
281 | + @return {String} The URL of the charm's icon. |
282 | + */ |
283 | + iconpath: function(charmID) { |
284 | + // If this is a local charm, then we need use a hard coded path to the |
285 | + // default icon since we cannot fetch its category data or its own |
286 | + // icon. |
287 | + // XXX: #1202703 - this is a short term fix for the bug. Need longer |
288 | + // term solution. |
289 | + if (charmID.indexOf('local:') === 0) { |
290 | + return this.get('apiHost') + |
291 | + 'static/img/charm_160.svg'; |
292 | + |
293 | + } else { |
294 | + // Get the charm ID from the service. In some cases, this will be |
295 | + // the charm URL with a protocol, which will need to be removed. |
296 | + // The following regular expression removes everything up to the |
297 | + // colon portion of the quote and leaves behind a charm ID. |
298 | + charmID = charmID.replace(/^[^:]+:/, ''); |
299 | + return this.get('apiHost') + [ |
300 | + this._apiRoot, |
301 | + 'charm', |
302 | + charmID, |
303 | + 'icon.svg'].join('/'); |
304 | + } |
305 | + }, |
306 | + |
307 | + /** |
308 | + * Fetch the interesting landing content from the charmworld api. |
309 | + * |
310 | + * @method interesting |
311 | + * @return {Object} data loaded from the api call. |
312 | + * |
313 | + */ |
314 | + interesting: function(callbacks, bindScope) { |
315 | + if (bindScope) { |
316 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
317 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
318 | + } |
319 | + |
320 | + this._makeRequest('charms/interesting', callbacks); |
321 | + } |
322 | + }, { |
323 | + ATTRS: {} |
324 | + }); |
325 | + |
326 | }, '0.1.0', { |
327 | requires: [ |
328 | 'datasource-io', |
329 | |
330 | === modified file 'test/test_charm_store.js' |
331 | --- test/test_charm_store.js 2013-09-17 17:56:07 +0000 |
332 | +++ test/test_charm_store.js 2013-09-17 19:00:33 +0000 |
333 | @@ -20,8 +20,244 @@ |
334 | |
335 | (function() { |
336 | |
337 | + describe('Charmworld API v3 interface', function() { |
338 | + var Y, models, conn, data, juju, utils, charmworld, hostname, api; |
339 | + |
340 | + |
341 | + before(function(done) { |
342 | + Y = YUI(GlobalConfig).use( |
343 | + 'datasource-local', 'json-stringify', 'juju-charm-store', |
344 | + 'datasource-io', 'io', 'array-extras', 'juju-charm-models', |
345 | + 'juju-tests-utils', |
346 | + function(Y) { |
347 | + juju = Y.namespace('juju'); |
348 | + charmworld = Y.namespace('juju.charmworld'); |
349 | + models = Y.namespace('juju.models'); |
350 | + utils = Y.namespace('juju-tests').utils; |
351 | + done(); |
352 | + }); |
353 | + }); |
354 | + |
355 | + beforeEach(function() { |
356 | + hostname = 'http://charmworld.example/'; |
357 | + api = new charmworld.APIv3({apiHost: hostname}); |
358 | + }); |
359 | + |
360 | + it('constructs the api url correctly based on apiHost', function() { |
361 | + var ds = api.get('datasource'); |
362 | + |
363 | + ds.get('source').should.eql(hostname + 'api/3/'); |
364 | + |
365 | + // And it should work without a trailing / as well. |
366 | + hostname = hostname.slice(0, -1); |
367 | + api = new charmworld.APIv3({apiHost: hostname}); |
368 | + ds = api.get('datasource'); |
369 | + ds.get('source').should.eql(hostname + '/api/3/'); |
370 | + }); |
371 | + |
372 | + it('handles loading interesting content correctly', function(done) { |
373 | + var data = []; |
374 | + |
375 | + data.push({responseText: Y.JSON.stringify({summary: 'wowza'})}); |
376 | + api.set('datasource', new Y.DataSource.Local({source: data})); |
377 | + |
378 | + api.interesting({ |
379 | + success: function(data) { |
380 | + data.summary.should.equal('wowza'); |
381 | + done(); |
382 | + }, |
383 | + failure: function(data, request) { |
384 | + } |
385 | + }, this); |
386 | + |
387 | + }); |
388 | + |
389 | + it('handles searching correctly', function(done) { |
390 | + var data = [], |
391 | + url; |
392 | + data.push({responseText: Y.JSON.stringify({name: 'foo'})}); |
393 | + // Create a monkeypatched datasource we can use to track the generated |
394 | + // apiEndpoint |
395 | + var datasource = new Y.DataSource.Local({source: data}); |
396 | + datasource.realSendRequest = datasource.sendRequest; |
397 | + datasource.sendRequest = function(params) { |
398 | + url = params.request; |
399 | + datasource.realSendRequest(params); |
400 | + }; |
401 | + |
402 | + api.set('datasource', datasource); |
403 | + api.search({text: 'foo'}, { |
404 | + success: function(data) { |
405 | + assert.equal('search?text=foo', url); |
406 | + assert.equal('foo', data.name); |
407 | + done(); |
408 | + }, |
409 | + failure: function(data, request) { |
410 | + } |
411 | + }, this); |
412 | + api.destroy(); |
413 | + }); |
414 | + |
415 | + it('constructs cateogry icon paths correctly', function() { |
416 | + var iconPath = api.buildCategoryIconPath('app-servers'); |
417 | + assert.equal( |
418 | + iconPath, |
419 | + hostname + 'static/img/category-app-servers-bw.svg'); |
420 | + }); |
421 | + |
422 | + it('makes charm requests to correct URL', function(done) { |
423 | + api._makeRequest = function(endpoint, callbacks, filters) { |
424 | + assert.equal(endpoint, 'charm/CHARM-ID'); |
425 | + done(); |
426 | + }; |
427 | + |
428 | + api._charm('CHARM-ID'); |
429 | + }); |
430 | + |
431 | + it('can use a cache to avoid requesting charm data', function(done) { |
432 | + var should_not_happen = function() { |
433 | + assert.isTrue(false, 'Oops, this should not have been called.'); |
434 | + done(); |
435 | + }; |
436 | + var CACHED_CHARM = 'CACHED-CHARM'; |
437 | + |
438 | + var callbacks = { |
439 | + success: function(data, charm) { |
440 | + assert.equal(charm, CACHED_CHARM); |
441 | + done(); |
442 | + }, |
443 | + failure: should_not_happen |
444 | + }; |
445 | + |
446 | + api._makeRequest = should_not_happen; |
447 | + |
448 | + var cache = { |
449 | + getById: function(charmID) { |
450 | + return CACHED_CHARM; |
451 | + }}; |
452 | + |
453 | + api.charm('CHARM-ID', callbacks, false, cache); |
454 | + |
455 | + }); |
456 | + |
457 | + it('will make a request on a cache miss', function(done) { |
458 | + var should_not_happen = function() { |
459 | + assert.isTrue(false, 'Oops, this should not have been called.'); |
460 | + done(); |
461 | + }; |
462 | + var CACHED_CHARM = 'CACHED-CHARM'; |
463 | + |
464 | + var callbacks = { |
465 | + success: function(data, charm) { |
466 | + assert.equal(charm, CACHED_CHARM); |
467 | + done(); |
468 | + }, |
469 | + failure: should_not_happen |
470 | + }; |
471 | + |
472 | + api._makeRequest = function() { |
473 | + // If this was called, then the test is successful. |
474 | + done(); |
475 | + }; |
476 | + |
477 | + var cache = { |
478 | + getById: function(charmID) { |
479 | + return null; |
480 | + }}; |
481 | + |
482 | + api.charm('CHARM-ID', callbacks, false, cache); |
483 | + |
484 | + }); |
485 | + |
486 | + it('makes autocomplete requests to correct URL', function(done) { |
487 | + var noop = function() {}; |
488 | + |
489 | + api._makeRequest = function(endpoint, callbacks, filters) { |
490 | + assert.equal(endpoint, 'search'); |
491 | + done(); |
492 | + }; |
493 | + |
494 | + api.autocomplete({text: 'mys'}, {'success': noop}); |
495 | + }); |
496 | + |
497 | + it('makes autocomplete requests with right query flag', function(done) { |
498 | + var noop = function() {}; |
499 | + |
500 | + api._makeRequest = function(endpoint, callbacks, filters) { |
501 | + assert.equal(filters.autocomplete, 'true'); |
502 | + done(); |
503 | + }; |
504 | + |
505 | + api.autocomplete({text: 'mys'}, {'success': noop}); |
506 | + }); |
507 | + |
508 | + it('constructs iconpaths correctly', function() { |
509 | + var iconPath = api.iconpath('precise/mysql-1'); |
510 | + assert.equal( |
511 | + iconPath, |
512 | + hostname + 'api/3/charm/precise/mysql-1/file/icon.svg'); |
513 | + }); |
514 | + |
515 | + it('constructs an icon path for local charms', function() { |
516 | + var iconPath = api.iconpath('local:precise/mysql-1'); |
517 | + assert.equal(iconPath, hostname + 'static/img/charm_160.svg'); |
518 | + }); |
519 | + |
520 | + it('removes cs: from the icon path when necessary', function() { |
521 | + var iconPath = api.iconpath('cs:precise/mysql-1'); |
522 | + assert.equal( |
523 | + iconPath, |
524 | + hostname + 'api/3/charm/precise/mysql-1/file/icon.svg'); |
525 | + }); |
526 | + |
527 | + it('can fetch a charm via a promise', function(done) { |
528 | + // The "promiseCharm" method is just a promise-wrapped version of the |
529 | + // "charm" method. |
530 | + var DATA = 'DATA'; |
531 | + var CHARM = 'CHARM'; |
532 | + api.charm = function(charmID, callbacks) { |
533 | + callbacks.success(DATA, CHARM); |
534 | + }; |
535 | + api.promiseCharm('CHARM-ID', null, 'precise') |
536 | + .then(function(data) { |
537 | + assert.equal(data, DATA); |
538 | + done(); |
539 | + }); |
540 | + }); |
541 | + |
542 | + it('finds upgrades for charms - upgrade available', function(done) { |
543 | + var store = utils.makeFakeStore(); |
544 | + var charm = new models.Charm({url: 'cs:precise/wordpress-10'}); |
545 | + store.promiseUpgradeAvailability(charm) |
546 | + .then(function(upgrade) { |
547 | + assert.equal(upgrade, 'precise/wordpress-15'); |
548 | + done(); |
549 | + }, function(error) { |
550 | + assert.isTrue(false, 'We should not get here.'); |
551 | + done(); |
552 | + }); |
553 | + }); |
554 | + |
555 | + it('finds upgrades for charms - no upgrade available', function(done) { |
556 | + var store = utils.makeFakeStore(); |
557 | + var charm = new models.Charm({url: 'cs:precise/wordpress-15'}); |
558 | + store.promiseUpgradeAvailability(charm) |
559 | + .then(function(upgrade) { |
560 | + assert.isUndefined(upgrade); |
561 | + done(); |
562 | + }, function(error) { |
563 | + assert.isTrue(false, 'We should not get here'); |
564 | + done(); |
565 | + }); |
566 | + }); |
567 | + |
568 | + }); |
569 | + |
570 | + // The tests below are based on copies of the v3 tests above so that removing |
571 | + // support for the v2 charmworld API will be easy. However, it means that |
572 | + // any edits made to these tests may need to be made to the v3 tests above. |
573 | describe('Charmworld API v2 interface', function() { |
574 | - var Y, models, conn, env, app, container, data, juju, utils, charmworld, |
575 | + var Y, models, conn, data, juju, utils, charmworld, |
576 | hostname, api; |
577 | |
578 | |
579 | @@ -99,13 +335,6 @@ |
580 | api.destroy(); |
581 | }); |
582 | |
583 | - it('constructs filepaths correctly', function() { |
584 | - var iconPath = api.filepath('precise/mysql-1', 'icon.svg'); |
585 | - assert.equal( |
586 | - iconPath, |
587 | - hostname + 'api/2/charm/precise/mysql-1/file/icon.svg'); |
588 | - }); |
589 | - |
590 | it('constructs cateogry icon paths correctly', function() { |
591 | var iconPath = api.buildCategoryIconPath('app-servers'); |
592 | assert.equal( |
593 | @@ -258,8 +487,7 @@ |
594 | }); |
595 | |
596 | describe('Charmworld API Helper', function() { |
597 | - var Y, models, conn, env, app, container, data, juju, utils, charmworld, |
598 | - hostname; |
599 | + var Y, models, conn, data, juju, utils, charmworld, hostname; |
600 | |
601 | |
602 | before(function(done) { |
603 | @@ -282,20 +510,60 @@ |
604 | |
605 | it('can normalize charm names for lookup', function() { |
606 | var apiHelper = new charmworld.ApiHelper({}); |
607 | + // If the charm ID does not include a series, the given default seriese |
608 | + // is used to fill our the charm ID. |
609 | assert.equal(apiHelper.normalizeCharmId('wordpress', 'precise'), |
610 | - 'precise/wordpress-1'); |
611 | - assert.equal(apiHelper.normalizeCharmId('precise/wordpress', 'precise'), |
612 | - 'precise/wordpress-1'); |
613 | + 'precise/wordpress'); |
614 | + // If no default series is given, "precise" is used. |
615 | + assert.equal(apiHelper.normalizeCharmId('wordpress'), |
616 | + 'precise/wordpress'); |
617 | + // If a series is provided, the default serise is ignored. |
618 | + assert.equal(apiHelper.normalizeCharmId('quantal/wordpress', 'precise'), |
619 | + 'quantal/wordpress'); |
620 | + // A charm ID with series and name but no revision is unchanged. |
621 | assert.equal(apiHelper.normalizeCharmId('precise/wordpress'), |
622 | - 'precise/wordpress-1'); |
623 | + 'precise/wordpress'); |
624 | + // A charm ID with series, name, and revision is unchanged. |
625 | assert.equal(apiHelper.normalizeCharmId('precise/wordpress-10'), |
626 | 'precise/wordpress-10'); |
627 | + // A leading charm store scheme identifier will be stripped. |
628 | assert.equal(apiHelper.normalizeCharmId('cs:precise/wordpress-10'), |
629 | 'precise/wordpress-10'); |
630 | - assert.equal(apiHelper.normalizeCharmId('precise/wordpress-HEAD'), |
631 | - 'precise/wordpress-HEAD'); |
632 | - assert.equal(apiHelper.normalizeCharmId('cs:precise/wordpress-HEAD'), |
633 | - 'precise/wordpress-HEAD'); |
634 | + }); |
635 | + |
636 | + }); |
637 | + |
638 | + describe('Charmworld API feature flag support', function() { |
639 | + var Y, models, conn, juju, app; |
640 | + |
641 | + |
642 | + before(function(done) { |
643 | + Y = YUI(GlobalConfig).use( |
644 | + 'juju-gui', |
645 | + 'datasource-local', 'json-stringify', 'juju-charm-store', |
646 | + 'datasource-io', 'io', 'array-extras', 'juju-charm-models', |
647 | + 'juju-tests-utils', |
648 | + function(Y) { |
649 | + juju = Y.namespace('juju'); |
650 | + done(); |
651 | + }); |
652 | + }); |
653 | + |
654 | + afterEach(function() { |
655 | + app.destroy(); |
656 | + window.flags = {}; |
657 | + }); |
658 | + |
659 | + it('enables the charmworld v2 API if not set', function() { |
660 | + assert.deepEqual(window.flags, {}); |
661 | + app = new Y.juju.App({}); |
662 | + assert.equal(app.get('store').name, 'APIv2'); |
663 | + }); |
664 | + |
665 | + it('enables the charmworld v3 API if set', function() { |
666 | + window.flags.charmworldv3 = true; |
667 | + app = new Y.juju.App({}); |
668 | + assert.equal(app.get('store').name, 'APIv3'); |
669 | }); |
670 | |
671 | }); |
LGTM with the config revert
#9 - revert config change