Merge lp:~benji/juju-gui/charmworld-api-3 into lp:juju-gui/experimental

Proposed by Benji York
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
Reviewer Review Type Date Requested Status
Richard Harding Approve
Review via email: mp+185325@code.launchpad.net

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.

https://codereview.appspot.com/13368056/

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

LGTM with the config revert

#9 - revert config change

review: Approve
Revision history for this message
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.

Revision history for this message
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://codereview.appspot.com/13368056/diff/1/app/store/charm.js
File app/store/charm.js (left):

https://codereview.appspot.com/13368056/diff/1/app/store/charm.js#oldcode114
app/store/charm.js:114: result = result + '-1';
Don't we still want this for APIv2?

https://codereview.appspot.com/13368056/diff/1/app/store/charm.js#oldcode353
app/store/charm.js:353: @method filepath
why don't we need this now? was it never used? I don't see it in your
APIv2 so...I'm confused.

https://codereview.appspot.com/13368056/diff/1/app/store/charm.js
File app/store/charm.js (right):

https://codereview.appspot.com/13368056/diff/1/app/store/charm.js#newcode546
app/store/charm.js:546: * Charmworld API version 2 interface.
I'm assuming this has no changes?

https://codereview.appspot.com/13368056/

Revision history for this message
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://codereview.appspot.com/13368056/diff/1/app/store/charm.js
File app/store/charm.js (right):

https://codereview.appspot.com/13368056/diff/1/app/store/charm.js#newcode549
app/store/charm.js:549: * @extends {Base}
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>

https://codereview.appspot.com/13368056/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 });

Subscribers

People subscribed via source and target branches