Merge ~matt-goodall/snapstore-client:per-store-config into snapstore-client:master
- Git
- lp:~matt-goodall/snapstore-client
- per-store-config
- Merge into master
Status: | Merged |
---|---|
Approved by: | Matt Goodall |
Approved revision: | 819d7029bab7902b0b62c558e503e54fb497ae5f |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~matt-goodall/snapstore-client:per-store-config |
Merge into: | snapstore-client:master |
Diff against target: |
1307 lines (+402/-372) 11 files modified
README (+5/-17) snapstore (+9/-1) snapstore_client/config.py (+44/-125) snapstore_client/logic/login.py (+22/-11) snapstore_client/logic/overrides.py (+36/-4) snapstore_client/logic/tests/test_login.py (+83/-27) snapstore_client/logic/tests/test_overrides.py (+35/-11) snapstore_client/tests/test_config.py (+66/-63) snapstore_client/tests/test_webservices.py (+51/-56) snapstore_client/tests/testfixtures.py (+28/-27) snapstore_client/webservices.py (+23/-30) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matt Goodall (community) | Approve | ||
Adam Collard (community) | Approve | ||
Review via email: mp+331064@code.launchpad.net |
Commit message
Configure a "store" at login and use that config for future calls.
Replaces old config consisting of fixed service URLs (too fine-grained anyway) and per-store credentials. A store's config now consists of the store's URL, SSO's URL, and the user credentials (macaroons etc).
For now, there is only one store - called "default" - but the command line could be extended to login and then select a store with a different name.
Description of the change
The diff makes it look more complicated than it is I think.
To help review ...
The big difference is that there's no global/fixed state any more - config is loaded at the start of a command and explicitly passed through to things like the webservices calls. No more importing config from files relative to Python modules \o/.
As a result, config management changed quite a lot although was largely based on the old Credentials bit of the config. It's actually simpler now.
Adam Collard (adam-collard) : | # |
Matt Goodall (matt-goodall) : | # |
Otto Co-Pilot (otto-copilot) wrote : | # |
Matt Goodall (matt-goodall) : | # |
Otto Co-Pilot (otto-copilot) wrote : | # |
Running landing tests failed
https:/
Preview Diff
1 | diff --git a/README b/README |
2 | index 8c40b12..6171594 100644 |
3 | --- a/README |
4 | +++ b/README |
5 | @@ -1,20 +1,8 @@ |
6 | -SIAB client script. |
7 | -=================== |
8 | +snapstore administration client |
9 | +=============================== |
10 | |
11 | +This repo contains a client to adminster a snapstore. |
12 | |
13 | -This repo contains a few points of interest: |
14 | +After authenticating with a snapstore the administrator can: |
15 | |
16 | - * The serve-snaps.py script will start a simple HTTP server to serve snap |
17 | - files from 'snap_storage'. Please don't use this in production, it's only |
18 | - supposed to be used for dev setups. |
19 | - |
20 | - * The snapstore script is a CLI UI to administer the snap store. Currently the |
21 | - only command is 'upload'. It can do two things: |
22 | - |
23 | - - upload a snap to the store, but don't release it:: |
24 | - |
25 | - $ snapstore upload /path/to/file.snap |
26 | - |
27 | - - upload and release a snap to a given channel: |
28 | - |
29 | - $ snapstore upload --channel /path/to/file.snap |
30 | + - manage revision overrides |
31 | diff --git a/snapstore b/snapstore |
32 | index ec0978d..9c2b0b1 100755 |
33 | --- a/snapstore |
34 | +++ b/snapstore |
35 | @@ -17,6 +17,9 @@ from snapstore_client.logic.overrides import ( |
36 | ) |
37 | |
38 | |
39 | +DEFAULT_SSO_URL = 'https://login.ubuntu.com/' |
40 | + |
41 | + |
42 | def main(): |
43 | configure_logging() |
44 | args = parse_args() |
45 | @@ -27,8 +30,13 @@ def parse_args(): |
46 | parser = argparse.ArgumentParser() |
47 | subparsers = parser.add_subparsers(help='sub-command help') |
48 | |
49 | - login_parser = subparsers.add_parser('login', help='Sign into a store.') |
50 | + login_parser = subparsers.add_parser( |
51 | + 'login', help='Sign into a store.', |
52 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
53 | + login_parser.add_argument('store_url', help='Store URL') |
54 | login_parser.add_argument('email', help='Ubuntu One SSO email', nargs='?') |
55 | + login_parser.add_argument('--sso-url', help='Ubuntu One SSO URL', |
56 | + default=DEFAULT_SSO_URL) |
57 | login_parser.set_defaults(func=login) |
58 | |
59 | list_overrides_parser = subparsers.add_parser( |
60 | diff --git a/snapstore_client/config.py b/snapstore_client/config.py |
61 | index 94df99b..ca7beb9 100644 |
62 | --- a/snapstore_client/config.py |
63 | +++ b/snapstore_client/config.py |
64 | @@ -6,150 +6,69 @@ |
65 | |
66 | import configparser |
67 | import os |
68 | -from urllib.parse import urlparse |
69 | |
70 | from xdg import BaseDirectory |
71 | |
72 | |
73 | -ROOT = os.path.abspath( |
74 | - os.path.join( |
75 | - os.path.dirname(__file__), |
76 | - '..' |
77 | - ) |
78 | -) |
79 | +class Config: |
80 | |
81 | - |
82 | -DEFAULT_TEST_CONFIG = { |
83 | - 'services': { |
84 | - 'snapdevicegw': 'http://localhost:8000/', |
85 | - 'storage': 'http://localhost:8005/', |
86 | - } |
87 | -} |
88 | - |
89 | - |
90 | -def _production_read_config(): |
91 | - root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) |
92 | - config = configparser.ConfigParser(dict(ROOT=root)) |
93 | - default_path = os.path.join(root, 'service.conf') |
94 | - config.read(default_path) |
95 | - return {sec: dict(config.items(sec)) for sec in config.sections()} |
96 | - |
97 | - |
98 | -class ConfigProvider(object): |
99 | - |
100 | - """Provide a single entry point for reading config that can be altered. |
101 | - |
102 | - This class allows production code to call 'read_config' and get a copy of |
103 | - test configuration during test runs, but production config during normal |
104 | - operations. |
105 | - """ |
106 | - |
107 | - def __init__(self, default_test_config): |
108 | - self._test_override = [] |
109 | - self._default_test_config = default_test_config |
110 | - |
111 | - def __call__(self): |
112 | - if self._test_override: |
113 | - return self._test_override[-1] |
114 | - else: |
115 | - return _production_read_config() |
116 | - |
117 | - def merge_config(self, config, new_config): |
118 | - merged_config = config.copy() |
119 | - for key in new_config.keys(): |
120 | - if key in config.keys(): |
121 | - if isinstance(config[key], dict) and \ |
122 | - isinstance(new_config[key], dict): |
123 | - merged_config[key] = self.merge_config( |
124 | - config[key], new_config[key]) |
125 | - else: |
126 | - merged_config[key] = new_config[key] |
127 | - else: |
128 | - merged_config[key] = new_config[key] |
129 | - return merged_config |
130 | - |
131 | - def override_for_test(self, new_config): |
132 | - """Override configuration for a test. |
133 | - |
134 | - In a test, if you wanted to set a value, you can do this:: |
135 | - |
136 | - from snapdevicegw.config import read_config |
137 | - |
138 | - def test_something(self): |
139 | - new_config = { |
140 | - 'worker': { |
141 | - 'some_setting': 'some_value' |
142 | - } |
143 | - } |
144 | - self.addCleanup(read_config.override_for_test(new_config)) |
145 | - |
146 | - The configuration provided is merged on top of DEFAULT_TEST_CONFIG |
147 | - (see above), so there's no need to specify values that are in the |
148 | - default config. |
149 | - """ |
150 | - if self._test_override: |
151 | - source = self._test_override[-1] |
152 | - else: |
153 | - source = self._default_test_config.copy() |
154 | - config = self.merge_config(source, new_config) |
155 | - self._test_override.append(config) |
156 | - return self._test_override.pop |
157 | - |
158 | - |
159 | -read_config = ConfigProvider(DEFAULT_TEST_CONFIG) |
160 | - |
161 | - |
162 | -class Credentials: |
163 | - """Per-user credentials storage.""" |
164 | + xdg_name = 'snapstore-client' |
165 | |
166 | def __init__(self): |
167 | self.parser = configparser.ConfigParser() |
168 | self.load() |
169 | |
170 | - @property |
171 | - def _section_name(self): |
172 | - # The only section we care about is the host from the snapdevicegw |
173 | - # URL. |
174 | - return urlparse(read_config()['services']['snapdevicegw']).netloc |
175 | + def load(self): |
176 | + path = BaseDirectory.load_first_config( |
177 | + self.xdg_name, 'config.ini') |
178 | + if path is not None and os.path.exists(path): |
179 | + self.parser.read(path) |
180 | + |
181 | + def save(self): |
182 | + path = os.path.join( |
183 | + BaseDirectory.save_config_path(self.xdg_name), |
184 | + 'config.ini') |
185 | + # TODO: better to write atomically |
186 | + with open(path, 'w') as f: |
187 | + self.parser.write(f) |
188 | |
189 | - def get(self, option_name): |
190 | + def get(self, section, option): |
191 | try: |
192 | - return self.parser.get(self._section_name, option_name) |
193 | + return self.parser.get(section, option) |
194 | except (configparser.NoSectionError, |
195 | configparser.NoOptionError, |
196 | KeyError): |
197 | return None |
198 | |
199 | - def set(self, option_name, value): |
200 | - section_name = self._section_name |
201 | - if not self.parser.has_section(section_name): |
202 | - self.parser.add_section(section_name) |
203 | - return self.parser.set(section_name, option_name, value) |
204 | + def set(self, section, option, value): |
205 | + if not self.parser.has_section(section): |
206 | + self.parser.add_section(section) |
207 | + return self.parser.set(section, option, value) |
208 | |
209 | - def is_empty(self): |
210 | - # Only check the current section |
211 | - section_name = self._section_name |
212 | - if self.parser.has_section(section_name): |
213 | - if self.parser.options(section_name): |
214 | - return False |
215 | - return True |
216 | + def section(self, name): |
217 | + return Section(self, name) |
218 | |
219 | - def load(self): |
220 | - path = BaseDirectory.load_first_config( |
221 | - 'snapstore-client', 'credentials.cfg') |
222 | - if path is not None and os.path.exists(path): |
223 | - self.parser.read(path) |
224 | + def store_section(self, name): |
225 | + return Section(self, 'store:'+name) |
226 | |
227 | - @staticmethod |
228 | - def save_path(): |
229 | - return os.path.join( |
230 | - BaseDirectory.save_config_path('snapstore-client'), |
231 | - 'credentials.cfg') |
232 | |
233 | - def save(self): |
234 | - path = self.save_path() |
235 | - with open(path, 'w') as f: |
236 | - self.parser.write(f) |
237 | +class Section: |
238 | |
239 | - def clear(self): |
240 | - self.parser.remove_section(self._section_name) |
241 | + def __init__(self, config, name): |
242 | + self.config = config |
243 | + self.name = name |
244 | + |
245 | + def get(self, option): |
246 | + return self.config.get(self.name, option) |
247 | + |
248 | + def set(self, option, value): |
249 | + return self.config.set(self.name, option, value) |
250 | + |
251 | + # XXX: mostly a hack |
252 | + # I don't think it's nice to expose save like this because it implies only |
253 | + # the section is updated. However, the webservices code currently refreshes |
254 | + # a store's macaroon and therefore needs to update and save config. |
255 | + # A better approach may be to *always* save config at the end of the |
256 | + # process so the internal implementation doesn't care about persistence. |
257 | + def save(self): |
258 | + self.config.save() |
259 | diff --git a/snapstore_client/logic/login.py b/snapstore_client/logic/login.py |
260 | index 47ab6b8..2963aef 100644 |
261 | --- a/snapstore_client/logic/login.py |
262 | +++ b/snapstore_client/logic/login.py |
263 | @@ -16,9 +16,9 @@ from snapstore_client import ( |
264 | logger = logging.getLogger(__name__) |
265 | |
266 | |
267 | -def _extract_caveat_id(root_macaroon): |
268 | +def _extract_caveat_id(sso_url, root_macaroon): |
269 | macaroon = Macaroon.deserialize(root_macaroon) |
270 | - sso_host = urlparse(config.read_config()['services']['sso']).netloc |
271 | + sso_host = urlparse(sso_url).netloc |
272 | for caveat in macaroon.caveats: |
273 | if caveat.location == sso_host: |
274 | return caveat.caveat_id |
275 | @@ -27,22 +27,26 @@ def _extract_caveat_id(root_macaroon): |
276 | |
277 | |
278 | def login(args): |
279 | + # TODO: validate these before using to avoid ugly errors. |
280 | + gw_url = args.store_url |
281 | + sso_url = args.sso_url |
282 | + |
283 | logger.info('Enter your Ubuntu One SSO credentials.') |
284 | email = args.email |
285 | if not email: |
286 | email = input('Email: ') |
287 | password = getpass.getpass('Password: ') |
288 | |
289 | - root = ws.issue_store_admin() |
290 | - caveat_id = _extract_caveat_id(root) |
291 | + root = ws.issue_store_admin(gw_url) |
292 | + caveat_id = _extract_caveat_id(sso_url, root) |
293 | try: |
294 | try: |
295 | unbound_discharge = ws.get_sso_discharge( |
296 | - email, password, caveat_id) |
297 | + sso_url, email, password, caveat_id) |
298 | except exceptions.StoreTwoFactorAuthenticationRequired: |
299 | one_time_password = input('Second-factor auth: ') |
300 | unbound_discharge = ws.get_sso_discharge( |
301 | - email, password, caveat_id, |
302 | + sso_url, email, password, caveat_id, |
303 | one_time_password=one_time_password) |
304 | except exceptions.StoreAuthenticationError as e: |
305 | logger.error('Login failed.') |
306 | @@ -54,10 +58,17 @@ def login(args): |
307 | logger.error('%s: %s', key, value) |
308 | return 1 |
309 | |
310 | - credentials = config.Credentials() |
311 | - credentials.set('root', root) |
312 | - credentials.set('unbound_discharge', unbound_discharge) |
313 | - credentials.set('email', email) |
314 | - credentials.save() |
315 | + cfg = config.Config() |
316 | + # For now, the store is always called "default". In the future we may want |
317 | + # to support multiple stores by allowing the user to provide a nice name |
318 | + # for a store at login that can be used to select the store for later |
319 | + # operations. |
320 | + store = cfg.store_section('default') |
321 | + store.set('gw_url', gw_url) |
322 | + store.set('sso_url', sso_url) |
323 | + store.set('root', root) |
324 | + store.set('unbound_discharge', unbound_discharge) |
325 | + store.set('email', email) |
326 | + cfg.save() |
327 | |
328 | return 0 |
329 | diff --git a/snapstore_client/logic/overrides.py b/snapstore_client/logic/overrides.py |
330 | index f514c48..54e421e 100644 |
331 | --- a/snapstore_client/logic/overrides.py |
332 | +++ b/snapstore_client/logic/overrides.py |
333 | @@ -5,6 +5,7 @@ import logging |
334 | from requests.exceptions import HTTPError |
335 | |
336 | from snapstore_client import ( |
337 | + config, |
338 | exceptions, |
339 | webservices as ws, |
340 | ) |
341 | @@ -19,13 +20,30 @@ logger = logging.getLogger(__name__) |
342 | |
343 | def _log_credentials_error(e): |
344 | logger.error('%s', e) |
345 | - logger.error('Have you run "snapstore login"?') |
346 | + logger.error('Try to "snapstore login" again.') |
347 | + |
348 | + |
349 | +def _check_default_store(cfg): |
350 | + """Load the default store from the config.""" |
351 | + store = cfg.store_section('default') |
352 | + # If the gw URL is configured then everything else should be too. |
353 | + if not store.get('gw_url'): |
354 | + logger.error( |
355 | + 'No store configuration found. Have you run "snapstore login"?') |
356 | + return None |
357 | + return store |
358 | |
359 | |
360 | def list_overrides(args): |
361 | + cfg = config.Config() |
362 | + store = _check_default_store(cfg) |
363 | + if not store: |
364 | + return 1 |
365 | + |
366 | try: |
367 | response = ws.refresh_if_necessary( |
368 | - ws.get_overrides, args.snap_name, series=args.series) |
369 | + store, ws.get_overrides, |
370 | + store, args.snap_name, series=args.series) |
371 | except exceptions.InvalidCredentials as e: |
372 | _log_credentials_error(e) |
373 | return 1 |
374 | @@ -37,6 +55,11 @@ def list_overrides(args): |
375 | |
376 | |
377 | def override(args): |
378 | + cfg = config.Config() |
379 | + store = _check_default_store(cfg) |
380 | + if not store: |
381 | + return 1 |
382 | + |
383 | overrides = [] |
384 | for channel_map_entry in args.channel_map_entries: |
385 | channel, revision = channel_map_string_to_tuple(channel_map_entry) |
386 | @@ -47,7 +70,9 @@ def override(args): |
387 | 'series': args.series, |
388 | }) |
389 | try: |
390 | - response = ws.refresh_if_necessary(ws.set_overrides, overrides) |
391 | + response = ws.refresh_if_necessary( |
392 | + store, ws.set_overrides, |
393 | + store, overrides) |
394 | except exceptions.InvalidCredentials as e: |
395 | _log_credentials_error(e) |
396 | return 1 |
397 | @@ -59,6 +84,11 @@ def override(args): |
398 | |
399 | |
400 | def delete_override(args): |
401 | + cfg = config.Config() |
402 | + store = _check_default_store(cfg) |
403 | + if not store: |
404 | + return 1 |
405 | + |
406 | overrides = [] |
407 | for channel in args.channels: |
408 | overrides.append({ |
409 | @@ -68,7 +98,9 @@ def delete_override(args): |
410 | 'series': args.series, |
411 | }) |
412 | try: |
413 | - response = ws.refresh_if_necessary(ws.set_overrides, overrides) |
414 | + response = ws.refresh_if_necessary( |
415 | + store, ws.set_overrides, |
416 | + store, overrides) |
417 | except exceptions.InvalidCredentials as e: |
418 | _log_credentials_error(e) |
419 | return 1 |
420 | diff --git a/snapstore_client/logic/tests/test_login.py b/snapstore_client/logic/tests/test_login.py |
421 | index 20d9d74..25ef14f 100644 |
422 | --- a/snapstore_client/logic/tests/test_login.py |
423 | +++ b/snapstore_client/logic/tests/test_login.py |
424 | @@ -2,9 +2,8 @@ |
425 | |
426 | import json |
427 | from unittest import mock |
428 | -from urllib.parse import urljoin |
429 | +from urllib.parse import urljoin, urlparse |
430 | |
431 | -from acceptable._doubles import set_service_locations |
432 | import fixtures |
433 | from pymacaroons import Macaroon |
434 | import responses |
435 | @@ -20,20 +19,15 @@ from snapstore_client import ( |
436 | exceptions, |
437 | ) |
438 | from snapstore_client.logic.login import login |
439 | -from snapstore_client.tests import ( |
440 | - factory, |
441 | - testfixtures, |
442 | -) |
443 | +from snapstore_client.tests import factory |
444 | |
445 | |
446 | class LoginTests(TestCase): |
447 | |
448 | def setUp(self): |
449 | super().setUp() |
450 | - self.useFixture(testfixtures.ConfigOverrideFixture( |
451 | - {'services': {'sso': 'https://login.example.com/'}})) |
452 | - service_locations = config.read_config()['services'] |
453 | - set_service_locations(service_locations) |
454 | + self.default_gw_url = 'http://store.local/' |
455 | + self.default_sso_url = 'https://login.staging.ubuntu.com/' |
456 | self.logger = self.useFixture(fixtures.FakeLogger()) |
457 | self.config_path = self.useFixture(fixtures.TempDir()).path |
458 | self.useFixture(fixtures.MonkeyPatch( |
459 | @@ -60,27 +54,33 @@ class LoginTests(TestCase): |
460 | iter_responses = iter(full_responses) |
461 | return lambda request: next(iter_responses) |
462 | |
463 | - def make_args(self, email=None): |
464 | - return factory.Args(email=email) |
465 | + def make_args(self, store_url=None, sso_url=None, email=None): |
466 | + return factory.Args( |
467 | + store_url=store_url or self.default_gw_url, |
468 | + sso_url=sso_url or self.default_sso_url, |
469 | + email=email, |
470 | + ) |
471 | |
472 | - def add_issue_store_admin_response(self, *response_templates): |
473 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
474 | - issue_store_admin_url = urljoin( |
475 | - devicegw_root, '/v2/auth/issue-store-admin') |
476 | + def add_issue_store_admin_response(self, *response_templates, gw_url=None): |
477 | + gw_url = gw_url or self.default_gw_url |
478 | + issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
479 | responses.add_callback( |
480 | 'POST', issue_store_admin_url, |
481 | self.make_responses_callback(response_templates)) |
482 | |
483 | - def add_get_sso_discharge_response(self, *response_templates): |
484 | - sso_root = config.read_config()['services']['sso'] |
485 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
486 | + def add_get_sso_discharge_response(self, *response_templates, |
487 | + sso_url=None): |
488 | + sso_url = sso_url or self.default_sso_url |
489 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
490 | responses.add_callback( |
491 | 'POST', discharge_url, |
492 | self.make_responses_callback(response_templates)) |
493 | |
494 | - def make_root_macaroon(self): |
495 | + def make_root_macaroon(self, sso_url=None): |
496 | macaroon = Macaroon() |
497 | - macaroon.add_third_party_caveat('login.example.com', 'key', 'payload') |
498 | + sso_url = sso_url or self.default_sso_url |
499 | + sso_host = urlparse(sso_url).netloc |
500 | + macaroon.add_third_party_caveat(sso_host, 'key', 'payload') |
501 | return macaroon.serialize() |
502 | |
503 | @responses.activate |
504 | @@ -161,8 +161,10 @@ class LoginTests(TestCase): |
505 | 'caveat_id': 'payload', |
506 | 'otp': '123456', |
507 | }, json.loads(responses.calls[2].request.body.decode())) |
508 | - self.assertThat(config.Credentials().parser, ContainsDict({ |
509 | - 'localhost:8000': MatchesDict({ |
510 | + self.assertThat(config.Config().parser, ContainsDict({ |
511 | + 'store:default': MatchesDict({ |
512 | + 'gw_url': Equals(self.default_gw_url), |
513 | + 'sso_url': Equals(self.default_sso_url), |
514 | 'root': Equals(root), |
515 | 'unbound_discharge': Equals('dummy'), |
516 | 'email': Equals('user@example.org'), |
517 | @@ -190,8 +192,10 @@ class LoginTests(TestCase): |
518 | 'password': 'secret', |
519 | 'caveat_id': 'payload', |
520 | }, json.loads(responses.calls[1].request.body.decode())) |
521 | - self.assertThat(config.Credentials().parser, ContainsDict({ |
522 | - 'localhost:8000': MatchesDict({ |
523 | + self.assertThat(config.Config().parser, ContainsDict({ |
524 | + 'store:default': MatchesDict({ |
525 | + 'gw_url': Equals(self.default_gw_url), |
526 | + 'sso_url': Equals(self.default_sso_url), |
527 | 'root': Equals(root), |
528 | 'unbound_discharge': Equals('dummy'), |
529 | 'email': Equals('user@example.org'), |
530 | @@ -219,10 +223,62 @@ class LoginTests(TestCase): |
531 | 'password': 'secret', |
532 | 'caveat_id': 'payload', |
533 | }, json.loads(responses.calls[1].request.body.decode())) |
534 | - self.assertThat(config.Credentials().parser, ContainsDict({ |
535 | - 'localhost:8000': MatchesDict({ |
536 | + self.assertThat(config.Config().parser, ContainsDict({ |
537 | + 'store:default': MatchesDict({ |
538 | + 'gw_url': Equals(self.default_gw_url), |
539 | + 'sso_url': Equals(self.default_sso_url), |
540 | 'root': Equals(root), |
541 | 'unbound_discharge': Equals('dummy'), |
542 | 'email': Equals('user@example.org'), |
543 | }), |
544 | })) |
545 | + |
546 | + @responses.activate |
547 | + def test_store_url(self): |
548 | + gw_url = 'http://otherstore.local:1234/' |
549 | + |
550 | + self.mock_input.return_value = 'user@example.org' |
551 | + self.mock_getpass.return_value = 'secret' |
552 | + root = self.make_root_macaroon() |
553 | + self.add_issue_store_admin_response( |
554 | + {'status': 200, 'json': {'macaroon': root}}, gw_url=gw_url) |
555 | + self.add_get_sso_discharge_response( |
556 | + {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}) |
557 | + login(self.make_args(store_url=gw_url)) |
558 | + |
559 | + self.assertEqual(2, len(responses.calls)) |
560 | + self.assertEqual(responses.calls[0].request.url[:len(gw_url)], gw_url) |
561 | + self.assertTrue( |
562 | + responses.calls[1].request.url.startswith(self.default_sso_url)) |
563 | + self.assertThat(config.Config().parser, ContainsDict({ |
564 | + 'store:default': ContainsDict({ |
565 | + 'gw_url': Equals(gw_url), |
566 | + 'sso_url': Equals(self.default_sso_url), |
567 | + }), |
568 | + })) |
569 | + |
570 | + @responses.activate |
571 | + def test_sso_url(self): |
572 | + sso_url = 'https://othersso.local:1234/' |
573 | + |
574 | + self.mock_input.return_value = 'user@example.org' |
575 | + self.mock_getpass.return_value = 'secret' |
576 | + root = self.make_root_macaroon(sso_url=sso_url) |
577 | + self.add_issue_store_admin_response( |
578 | + {'status': 200, 'json': {'macaroon': root}}) |
579 | + self.add_get_sso_discharge_response( |
580 | + {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}, |
581 | + sso_url=sso_url) |
582 | + login(self.make_args(sso_url=sso_url)) |
583 | + |
584 | + self.assertEqual(2, len(responses.calls)) |
585 | + self.assertTrue( |
586 | + responses.calls[0].request.url.startswith(self.default_gw_url)) |
587 | + self.assertEqual( |
588 | + responses.calls[1].request.url[:len(sso_url)], sso_url) |
589 | + self.assertThat(config.Config().parser, ContainsDict({ |
590 | + 'store:default': ContainsDict({ |
591 | + 'gw_url': Equals(self.default_gw_url), |
592 | + 'sso_url': Equals(sso_url), |
593 | + }), |
594 | + })) |
595 | diff --git a/snapstore_client/logic/tests/test_overrides.py b/snapstore_client/logic/tests/test_overrides.py |
596 | index 80a929f..b92c966 100644 |
597 | --- a/snapstore_client/logic/tests/test_overrides.py |
598 | +++ b/snapstore_client/logic/tests/test_overrides.py |
599 | @@ -3,7 +3,6 @@ |
600 | import json |
601 | from urllib.parse import urljoin |
602 | |
603 | -from acceptable._doubles import set_service_locations |
604 | import fixtures |
605 | import responses |
606 | from testtools import TestCase |
607 | @@ -22,15 +21,19 @@ from snapstore_client.tests import ( |
608 | |
609 | class OverridesTests(TestCase): |
610 | |
611 | - def setUp(self): |
612 | - super().setUp() |
613 | - service_locations = config.read_config()['services'] |
614 | - set_service_locations(service_locations) |
615 | + def test_list_overrides_no_store_config(self): |
616 | + self.useFixture(testfixtures.ConfigFixture(empty=True)) |
617 | + logger = self.useFixture(fixtures.FakeLogger()) |
618 | + rc = list_overrides(factory.Args(snap_name='some-snap', series='16')) |
619 | + self.assertEqual(rc, 1) |
620 | + self.assertEqual( |
621 | + logger.output, |
622 | + 'No store configuration found. Have you run "snapstore login"?\n') |
623 | |
624 | @responses.activate |
625 | def test_list_overrides(self): |
626 | + self.useFixture(testfixtures.ConfigFixture()) |
627 | logger = self.useFixture(fixtures.FakeLogger()) |
628 | - self.useFixture(testfixtures.CredentialsFixture()) |
629 | snap_id = factory.generate_snap_id() |
630 | overrides = [ |
631 | factory.SnapDeviceGateway.Override( |
632 | @@ -43,7 +46,7 @@ class OverridesTests(TestCase): |
633 | # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
634 | # they exist. |
635 | overrides_url = urljoin( |
636 | - config.read_config()['services']['snapdevicegw'], |
637 | + config.Config().store_section('default').get('gw_url'), |
638 | '/v2/metadata/overrides/mysnap') |
639 | responses.add( |
640 | 'GET', overrides_url, status=200, json={'overrides': overrides}) |
641 | @@ -54,10 +57,21 @@ class OverridesTests(TestCase): |
642 | 'mysnap foo/stable i386 3 (upstream 4)\n', |
643 | logger.output) |
644 | |
645 | + def test_override_no_store_config(self): |
646 | + self.useFixture(testfixtures.ConfigFixture(empty=True)) |
647 | + logger = self.useFixture(fixtures.FakeLogger()) |
648 | + rc = override(factory.Args( |
649 | + snap_name='some-snap', channel_map_entries=['stable=1'], |
650 | + series='16')) |
651 | + self.assertEqual(rc, 1) |
652 | + self.assertEqual( |
653 | + logger.output, |
654 | + 'No store configuration found. Have you run "snapstore login"?\n') |
655 | + |
656 | @responses.activate |
657 | def test_override(self): |
658 | + self.useFixture(testfixtures.ConfigFixture()) |
659 | logger = self.useFixture(fixtures.FakeLogger()) |
660 | - self.useFixture(testfixtures.CredentialsFixture()) |
661 | snap_id = factory.generate_snap_id() |
662 | overrides = [ |
663 | factory.SnapDeviceGateway.Override( |
664 | @@ -70,7 +84,7 @@ class OverridesTests(TestCase): |
665 | # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
666 | # they exist. |
667 | overrides_url = urljoin( |
668 | - config.read_config()['services']['snapdevicegw'], |
669 | + config.Config().store_section('default').get('gw_url'), |
670 | '/v2/metadata/overrides') |
671 | responses.add( |
672 | 'POST', overrides_url, status=200, json={'overrides': overrides}) |
673 | @@ -98,10 +112,20 @@ class OverridesTests(TestCase): |
674 | 'mysnap foo/stable i386 3 (upstream 4)\n', |
675 | logger.output) |
676 | |
677 | + def test_delete_override_no_store_config(self): |
678 | + self.useFixture(testfixtures.ConfigFixture(empty=True)) |
679 | + logger = self.useFixture(fixtures.FakeLogger()) |
680 | + rc = delete_override(factory.Args( |
681 | + snap_name='some-snap', channels=['stable'], series='16')) |
682 | + self.assertEqual(rc, 1) |
683 | + self.assertEqual( |
684 | + logger.output, |
685 | + 'No store configuration found. Have you run "snapstore login"?\n') |
686 | + |
687 | @responses.activate |
688 | def test_delete_override(self): |
689 | + self.useFixture(testfixtures.ConfigFixture()) |
690 | logger = self.useFixture(fixtures.FakeLogger()) |
691 | - self.useFixture(testfixtures.CredentialsFixture()) |
692 | snap_id = factory.generate_snap_id() |
693 | overrides = [ |
694 | factory.SnapDeviceGateway.Override( |
695 | @@ -114,7 +138,7 @@ class OverridesTests(TestCase): |
696 | # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
697 | # they exist. |
698 | overrides_url = urljoin( |
699 | - config.read_config()['services']['snapdevicegw'], |
700 | + config.Config().store_section('default').get('gw_url'), |
701 | '/v2/metadata/overrides') |
702 | responses.add( |
703 | 'POST', overrides_url, status=200, json={'overrides': overrides}) |
704 | diff --git a/snapstore_client/tests/test_config.py b/snapstore_client/tests/test_config.py |
705 | index 0940828..2ddbc5c 100644 |
706 | --- a/snapstore_client/tests/test_config.py |
707 | +++ b/snapstore_client/tests/test_config.py |
708 | @@ -1,71 +1,74 @@ |
709 | # Copyright 2017 Canonical Ltd. This software is licensed under the |
710 | # GNU General Public License version 3 (see the file LICENSE). |
711 | |
712 | +import os.path |
713 | +from textwrap import dedent |
714 | + |
715 | from testtools import TestCase |
716 | |
717 | -from snapstore_client.config import ConfigProvider |
718 | - |
719 | - |
720 | -class ConfigProviderTestCase(TestCase): |
721 | - |
722 | - def test_override_works_for_new_section_and_option(self): |
723 | - default = {} |
724 | - override = { |
725 | - 'new_section': { |
726 | - 'new_option': 'new_value' |
727 | - } |
728 | - } |
729 | - expected = override.copy() |
730 | - |
731 | - provider = ConfigProvider(default) |
732 | - provider.override_for_test(override) |
733 | - |
734 | - self.assertEqual(expected, provider()) |
735 | - |
736 | - def test_override_works_when_adding_new_option_to_existing_section(self): |
737 | - default = { |
738 | - 'section': { |
739 | - 'option': 'value' |
740 | - } |
741 | - } |
742 | - override = { |
743 | - 'section': { |
744 | - 'new_option': 'new_value' |
745 | - } |
746 | - } |
747 | - expected = { |
748 | - 'section': { |
749 | - 'option': 'value', |
750 | - 'new_option': 'new_value' |
751 | - } |
752 | - } |
753 | - |
754 | - provider = ConfigProvider(default) |
755 | - provider.override_for_test(override) |
756 | - |
757 | - self.assertEqual(expected, provider()) |
758 | - |
759 | - def test_override_works_when_changing_existing_option(self): |
760 | - default = { |
761 | - 'section': { |
762 | - 'option': 'value' |
763 | - } |
764 | - } |
765 | - override = { |
766 | - 'section': { |
767 | - 'option': 'new_value' |
768 | - } |
769 | - } |
770 | - expected = { |
771 | - 'section': { |
772 | - 'option': 'new_value', |
773 | - } |
774 | - } |
775 | - |
776 | - provider = ConfigProvider(default) |
777 | - provider.override_for_test(override) |
778 | - |
779 | - self.assertEqual(expected, provider()) |
780 | +from snapstore_client.config import Config |
781 | +from snapstore_client.tests.testfixtures import ( |
782 | + ConfigFixture, |
783 | + XDGConfigDirFixture, |
784 | +) |
785 | + |
786 | + |
787 | +class ConfigTests(TestCase): |
788 | + |
789 | + def test_no_config(self): |
790 | + xdg_path = self.useFixture(XDGConfigDirFixture()).path |
791 | + config_ini = os.path.join(xdg_path, Config.xdg_name, 'config.ini') |
792 | + self.assertFalse(os.path.exists(config_ini)) |
793 | + Config() |
794 | + |
795 | + def test_init_loads(self): |
796 | + self.useFixture(ConfigFixture()) |
797 | + cfg = Config() |
798 | + self.assertIsNotNone(cfg.get("store:default", "gw_url")) |
799 | + |
800 | + def test_save(self): |
801 | + xdg_path = self.useFixture(XDGConfigDirFixture()).path |
802 | + config_ini = os.path.join(xdg_path, 'snapstore-client', 'config.ini') |
803 | + self.assertFalse(os.path.exists(config_ini)) |
804 | + |
805 | + cfg = Config() |
806 | + cfg.set("s", "k", "v") |
807 | + cfg.save() |
808 | + |
809 | + self.assertTrue(os.path.exists(config_ini)) |
810 | + with open(config_ini) as f: |
811 | + content = f.read() |
812 | + self.assertEqual(content, dedent('''\ |
813 | + [s] |
814 | + k = v |
815 | + |
816 | + ''')) |
817 | + |
818 | + def test_get_missing(self): |
819 | + self.useFixture(XDGConfigDirFixture()) |
820 | + cfg = Config() |
821 | + self.assertIsNone(cfg.get("s", "k")) |
822 | + |
823 | + def test_get(self): |
824 | + self.useFixture(XDGConfigDirFixture()) |
825 | + cfg = Config() |
826 | + cfg.set("s", "k", "v") |
827 | + self.assertEqual(cfg.get("s", "k"), "v") |
828 | + |
829 | + def test_section(self): |
830 | + self.useFixture(XDGConfigDirFixture()) |
831 | + cfg = Config() |
832 | + s = cfg.section("s") |
833 | + s.set("k", "v") |
834 | + self.assertEqual(s.get("k"), "v") |
835 | + self.assertEqual(cfg.get("s", "k"), "v") |
836 | + |
837 | + def test_store_section(self): |
838 | + self.useFixture(XDGConfigDirFixture()) |
839 | + cfg = Config() |
840 | + store = cfg.store_section("foo") |
841 | + store.set("k", "v") |
842 | + self.assertEqual(cfg.get("store:foo", "k"), "v") |
843 | |
844 | |
845 | def test_suite(): |
846 | diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py |
847 | index fd51f7e..f6bf6bf 100644 |
848 | --- a/snapstore_client/tests/test_webservices.py |
849 | +++ b/snapstore_client/tests/test_webservices.py |
850 | @@ -6,7 +6,6 @@ import sys |
851 | import types |
852 | from urllib.parse import urljoin |
853 | |
854 | -from acceptable._doubles import set_service_locations |
855 | import fixtures |
856 | from requests.exceptions import HTTPError |
857 | import responses |
858 | @@ -39,39 +38,37 @@ class WebservicesTests(TestCase): |
859 | |
860 | def setUp(self): |
861 | super().setUp() |
862 | - service_locations = config.read_config()['services'] |
863 | - set_service_locations(service_locations) |
864 | + self.config = self.useFixture(testfixtures.ConfigFixture()) |
865 | |
866 | @responses.activate |
867 | def test_issue_store_admin_success(self): |
868 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
869 | - issue_store_admin_url = urljoin( |
870 | - devicegw_root, '/v2/auth/issue-store-admin') |
871 | + gw_url = 'http://store.local/' |
872 | + issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
873 | responses.add( |
874 | 'POST', issue_store_admin_url, status=200, |
875 | json={'macaroon': 'dummy'}) |
876 | |
877 | - self.assertEqual('dummy', webservices.issue_store_admin()) |
878 | + self.assertEqual('dummy', webservices.issue_store_admin(gw_url)) |
879 | |
880 | @responses.activate |
881 | def test_issue_store_admin_error(self): |
882 | logger = self.useFixture(fixtures.FakeLogger()) |
883 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
884 | - issue_store_admin_url = urljoin( |
885 | - devicegw_root, '/v2/auth/issue-store-admin') |
886 | + gw_url = 'http://store.local/' |
887 | + issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
888 | responses.add( |
889 | 'POST', issue_store_admin_url, status=400, |
890 | json=factory.APIError.single('Something went wrong').to_dict()) |
891 | |
892 | - self.assertRaises(HTTPError, webservices.issue_store_admin) |
893 | + self.assertRaises( |
894 | + HTTPError, webservices.issue_store_admin, gw_url) |
895 | self.assertEqual( |
896 | 'Failed to issue store_admin macaroon:\nSomething went wrong\n', |
897 | logger.output) |
898 | |
899 | @responses.activate |
900 | def test_get_sso_discharge_success(self): |
901 | - sso_root = config.read_config()['services']['sso'] |
902 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
903 | + sso_url = 'http://sso.local/' |
904 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
905 | responses.add( |
906 | 'POST', discharge_url, status=200, |
907 | json={'discharge_macaroon': 'dummy'}) |
908 | @@ -79,7 +76,7 @@ class WebservicesTests(TestCase): |
909 | self.assertEqual( |
910 | 'dummy', |
911 | webservices.get_sso_discharge( |
912 | - 'user@example.org', 'secret', 'caveat')) |
913 | + sso_url, 'user@example.org', 'secret', 'caveat')) |
914 | request = responses.calls[0].request |
915 | self.assertEqual('application/json', request.headers['Content-Type']) |
916 | self.assertEqual({ |
917 | @@ -90,8 +87,8 @@ class WebservicesTests(TestCase): |
918 | |
919 | @responses.activate |
920 | def test_get_sso_discharge_success_with_otp(self): |
921 | - sso_root = config.read_config()['services']['sso'] |
922 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
923 | + sso_url = 'http://sso.local/' |
924 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
925 | responses.add( |
926 | 'POST', discharge_url, status=200, |
927 | json={'discharge_macaroon': 'dummy'}) |
928 | @@ -99,7 +96,7 @@ class WebservicesTests(TestCase): |
929 | self.assertEqual( |
930 | 'dummy', |
931 | webservices.get_sso_discharge( |
932 | - 'user@example.org', 'secret', 'caveat', |
933 | + sso_url, 'user@example.org', 'secret', 'caveat', |
934 | one_time_password='123456')) |
935 | request = responses.calls[0].request |
936 | self.assertEqual('application/json', request.headers['Content-Type']) |
937 | @@ -112,8 +109,8 @@ class WebservicesTests(TestCase): |
938 | |
939 | @responses.activate |
940 | def test_get_sso_discharge_twofactor_required(self): |
941 | - sso_root = config.read_config()['services']['sso'] |
942 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
943 | + sso_url = 'http://sso.local/' |
944 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
945 | responses.add( |
946 | 'POST', discharge_url, status=401, |
947 | json={'error_list': [{'code': 'twofactor-required'}]}) |
948 | @@ -121,32 +118,32 @@ class WebservicesTests(TestCase): |
949 | self.assertRaises( |
950 | exceptions.StoreTwoFactorAuthenticationRequired, |
951 | webservices.get_sso_discharge, |
952 | - 'user@example.org', 'secret', 'caveat') |
953 | + sso_url, 'user@example.org', 'secret', 'caveat') |
954 | |
955 | @responses.activate |
956 | def test_get_sso_discharge_structured_error(self): |
957 | - sso_root = config.read_config()['services']['sso'] |
958 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
959 | + sso_url = 'http://sso.local/' |
960 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
961 | responses.add( |
962 | 'POST', discharge_url, status=400, |
963 | json=factory.APIError.single('Something went wrong').to_dict()) |
964 | |
965 | e = self.assertRaises( |
966 | exceptions.StoreAuthenticationError, webservices.get_sso_discharge, |
967 | - 'user@example.org', 'secret', 'caveat') |
968 | + sso_url, 'user@example.org', 'secret', 'caveat') |
969 | self.assertEqual('Something went wrong', e.message) |
970 | |
971 | @responses.activate |
972 | def test_get_sso_discharge_unstructured_error(self): |
973 | logger = self.useFixture(fixtures.FakeLogger()) |
974 | - sso_root = config.read_config()['services']['sso'] |
975 | - discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge') |
976 | + sso_url = 'http://sso.local/' |
977 | + discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
978 | responses.add( |
979 | 'POST', discharge_url, status=503, body='Try again later.') |
980 | |
981 | self.assertRaises( |
982 | HTTPError, webservices.get_sso_discharge, |
983 | - 'user@example.org', 'secret', 'caveat') |
984 | + sso_url, 'user@example.org', 'secret', 'caveat') |
985 | self.assertEqual( |
986 | 'Failed to get SSO discharge:\n' |
987 | '====================\n' |
988 | @@ -157,59 +154,58 @@ class WebservicesTests(TestCase): |
989 | @responses.activate |
990 | def test_get_overrides_success(self): |
991 | logger = self.useFixture(fixtures.FakeLogger()) |
992 | - credentials = self.useFixture(testfixtures.CredentialsFixture()) |
993 | overrides = [factory.SnapDeviceGateway.Override()] |
994 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
995 | # exists. |
996 | + store = config.Config().store_section('default') |
997 | overrides_url = urljoin( |
998 | - config.read_config()['services']['snapdevicegw'], |
999 | - '/v2/metadata/overrides/mysnap') |
1000 | + store.get('gw_url'), '/v2/metadata/overrides/mysnap') |
1001 | responses.add('GET', overrides_url, status=200, json=overrides) |
1002 | |
1003 | - self.assertEqual(overrides, webservices.get_overrides('mysnap')) |
1004 | + self.assertEqual(overrides, webservices.get_overrides( |
1005 | + store, 'mysnap')) |
1006 | request = responses.calls[0].request |
1007 | self.assertThat( |
1008 | request.headers['Authorization'], |
1009 | - matchers.MacaroonHeaderVerifies(credentials.key)) |
1010 | + matchers.MacaroonHeaderVerifies(self.config.key)) |
1011 | self.assertNotIn('Failed to get overrides:', logger.output) |
1012 | |
1013 | @responses.activate |
1014 | def test_get_overrides_error(self): |
1015 | logger = self.useFixture(fixtures.FakeLogger()) |
1016 | - self.useFixture(testfixtures.CredentialsFixture()) |
1017 | + store = config.Config().store_section('default') |
1018 | overrides_url = urljoin( |
1019 | - config.read_config()['services']['snapdevicegw'], |
1020 | - '/v2/metadata/overrides/mysnap') |
1021 | + store.get('gw_url'), '/v2/metadata/overrides/mysnap') |
1022 | responses.add( |
1023 | 'GET', overrides_url, status=400, |
1024 | json=factory.APIError.single('Something went wrong').to_dict()) |
1025 | |
1026 | - self.assertRaises(HTTPError, webservices.get_overrides, 'mysnap') |
1027 | + self.assertRaises( |
1028 | + HTTPError, webservices.get_overrides, store, 'mysnap') |
1029 | self.assertEqual( |
1030 | 'Failed to get overrides:\nSomething went wrong\n', logger.output) |
1031 | |
1032 | @responses.activate |
1033 | def test_set_overrides_success(self): |
1034 | logger = self.useFixture(fixtures.FakeLogger()) |
1035 | - credentials = self.useFixture(testfixtures.CredentialsFixture()) |
1036 | override = factory.SnapDeviceGateway.Override() |
1037 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
1038 | # exists. |
1039 | - overrides_url = urljoin( |
1040 | - config.read_config()['services']['snapdevicegw'], |
1041 | - '/v2/metadata/overrides') |
1042 | + store = config.Config().store_section('default') |
1043 | + overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides') |
1044 | responses.add('POST', overrides_url, status=200, json=[override]) |
1045 | |
1046 | - self.assertEqual([override], webservices.set_overrides([{ |
1047 | - 'snap_name': override['snap_name'], |
1048 | - 'revision': override['revision'], |
1049 | - 'channel': override['channel'], |
1050 | - 'series': override['series'], |
1051 | - }])) |
1052 | + self.assertEqual([override], webservices.set_overrides( |
1053 | + store, [{ |
1054 | + 'snap_name': override['snap_name'], |
1055 | + 'revision': override['revision'], |
1056 | + 'channel': override['channel'], |
1057 | + 'series': override['series'], |
1058 | + }])) |
1059 | request = responses.calls[0].request |
1060 | self.assertThat( |
1061 | request.headers['Authorization'], |
1062 | - matchers.MacaroonHeaderVerifies(credentials.key)) |
1063 | + matchers.MacaroonHeaderVerifies(self.config.key)) |
1064 | self.assertEqual([{ |
1065 | 'snap_name': override['snap_name'], |
1066 | 'revision': override['revision'], |
1067 | @@ -221,22 +217,21 @@ class WebservicesTests(TestCase): |
1068 | @responses.activate |
1069 | def test_set_overrides_error(self): |
1070 | logger = self.useFixture(fixtures.FakeLogger()) |
1071 | - self.useFixture(testfixtures.CredentialsFixture()) |
1072 | override = factory.SnapDeviceGateway.Override() |
1073 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
1074 | # exists. |
1075 | - overrides_url = urljoin( |
1076 | - config.read_config()['services']['snapdevicegw'], |
1077 | - '/v2/metadata/overrides') |
1078 | + store = config.Config().store_section('default') |
1079 | + overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides') |
1080 | responses.add( |
1081 | 'POST', overrides_url, status=400, |
1082 | json=factory.APIError.single('Something went wrong').to_dict()) |
1083 | |
1084 | - self.assertRaises(HTTPError, webservices.set_overrides, { |
1085 | - 'snap_name': override['snap_name'], |
1086 | - 'revision': override['revision'], |
1087 | - 'channel': override['channel'], |
1088 | - 'series': override['series'], |
1089 | - }) |
1090 | + self.assertRaises(HTTPError, lambda: webservices.set_overrides( |
1091 | + store, { |
1092 | + 'snap_name': override['snap_name'], |
1093 | + 'revision': override['revision'], |
1094 | + 'channel': override['channel'], |
1095 | + 'series': override['series'], |
1096 | + })) |
1097 | self.assertEqual( |
1098 | 'Failed to set override:\nSomething went wrong\n', logger.output) |
1099 | diff --git a/snapstore_client/tests/testfixtures.py b/snapstore_client/tests/testfixtures.py |
1100 | index 5070d5a..78bf1dd 100644 |
1101 | --- a/snapstore_client/tests/testfixtures.py |
1102 | +++ b/snapstore_client/tests/testfixtures.py |
1103 | @@ -2,7 +2,6 @@ |
1104 | |
1105 | import os.path |
1106 | from textwrap import dedent |
1107 | -from urllib.parse import urlparse |
1108 | |
1109 | from fixtures import ( |
1110 | Fixture, |
1111 | @@ -11,23 +10,27 @@ from fixtures import ( |
1112 | ) |
1113 | from pymacaroons import Macaroon |
1114 | |
1115 | -from snapstore_client import config |
1116 | |
1117 | +class XDGConfigDirFixture(Fixture): |
1118 | + """Set up xdg to read/write config from temp dir.""" |
1119 | |
1120 | -class ConfigOverrideFixture(Fixture): |
1121 | - |
1122 | - def __init__(self, new_config): |
1123 | - super().__init__() |
1124 | - self._new_config = new_config |
1125 | + path = None |
1126 | |
1127 | def _setUp(self): |
1128 | - self.addCleanup(config.read_config.override_for_test(self._new_config)) |
1129 | + self.path = self.useFixture(TempDir()).path |
1130 | + self.useFixture(MonkeyPatch( |
1131 | + 'xdg.BaseDirectory.xdg_config_home', self.path)) |
1132 | + self.useFixture(MonkeyPatch( |
1133 | + 'xdg.BaseDirectory.xdg_config_dirs', [self.path])) |
1134 | |
1135 | |
1136 | -class CredentialsFixture(Fixture): |
1137 | +class ConfigFixture(Fixture): |
1138 | |
1139 | - def __init__(self): |
1140 | + def __init__(self, empty=False): |
1141 | super().__init__() |
1142 | + self.empty = empty |
1143 | + self.gw_url = 'http://store.local/' |
1144 | + self.sso_url = 'http://sso.local/' |
1145 | self.key = 'random-key' |
1146 | self.root = Macaroon(key=self.key) |
1147 | self.root.add_third_party_caveat( |
1148 | @@ -36,22 +39,20 @@ class CredentialsFixture(Fixture): |
1149 | location='login.example.com', identifier='payload', key='sso-key') |
1150 | |
1151 | def _setUp(self): |
1152 | - config_path = self.useFixture(TempDir()).path |
1153 | - credentials_path = os.path.join( |
1154 | - config_path, 'snapstore-client', 'credentials.cfg') |
1155 | - os.makedirs(os.path.dirname(credentials_path)) |
1156 | - snapdevicegw_netloc = urlparse( |
1157 | - config.read_config()['services']['snapdevicegw']).netloc |
1158 | - with open(credentials_path, 'w') as credentials_file: |
1159 | - credentials = dedent("""\ |
1160 | - [{snapdevicegw}] |
1161 | + xdg_config_path = self.useFixture(XDGConfigDirFixture()).path |
1162 | + app_config_path = os.path.join(xdg_config_path, 'snapstore-client') |
1163 | + os.makedirs(app_config_path) |
1164 | + if self.empty: |
1165 | + return |
1166 | + with open(os.path.join(app_config_path, 'config.ini'), 'w') as f: |
1167 | + f.write(dedent(""" |
1168 | + [store:default] |
1169 | + gw_url = {gw_url} |
1170 | + sso_url = {sso_url} |
1171 | root = {root} |
1172 | unbound_discharge = {unbound_discharge} |
1173 | - """).format( |
1174 | - snapdevicegw=snapdevicegw_netloc, |
1175 | - root=self.root.serialize(), |
1176 | - unbound_discharge=self.unbound_discharge.serialize(), |
1177 | - ) |
1178 | - print(credentials, file=credentials_file) |
1179 | - self.useFixture(MonkeyPatch( |
1180 | - 'xdg.BaseDirectory.xdg_config_dirs', [config_path])) |
1181 | + """).format( |
1182 | + gw_url=self.gw_url, sso_url=self.sso_url, |
1183 | + root=self.root.serialize(), |
1184 | + unbound_discharge=self.unbound_discharge.serialize(), |
1185 | + )) |
1186 | diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py |
1187 | index fc8c460..fb1927a 100644 |
1188 | --- a/snapstore_client/webservices.py |
1189 | +++ b/snapstore_client/webservices.py |
1190 | @@ -8,20 +8,16 @@ import urllib.parse |
1191 | from pymacaroons import Macaroon |
1192 | import requests |
1193 | |
1194 | -from snapstore_client import ( |
1195 | - config, |
1196 | - exceptions, |
1197 | -) |
1198 | +from snapstore_client import exceptions |
1199 | |
1200 | |
1201 | logger = logging.getLogger(__name__) |
1202 | |
1203 | |
1204 | -def issue_store_admin(): |
1205 | +def issue_store_admin(gw_url): |
1206 | """Ask the store to issue a store_admin macaroon.""" |
1207 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
1208 | issue_store_admin_url = urllib.parse.urljoin( |
1209 | - devicegw_root, '/v2/auth/issue-store-admin') |
1210 | + gw_url, '/v2/auth/issue-store-admin') |
1211 | resp = requests.post(issue_store_admin_url) |
1212 | if resp.status_code != 200: |
1213 | _print_error_message('issue store_admin macaroon', resp) |
1214 | @@ -29,9 +25,10 @@ def issue_store_admin(): |
1215 | return resp.json()['macaroon'] |
1216 | |
1217 | |
1218 | -def get_sso_discharge(email, password, caveat_id, one_time_password=None): |
1219 | - sso_root = config.read_config()['services']['sso'] |
1220 | - discharge_url = urllib.parse.urljoin(sso_root, '/api/v2/tokens/discharge') |
1221 | +def get_sso_discharge(sso_url, email, password, caveat_id, |
1222 | + one_time_password=None): |
1223 | + discharge_url = urllib.parse.urljoin( |
1224 | + sso_url, '/api/v2/tokens/discharge') |
1225 | data = {'email': email, 'password': password, 'caveat_id': caveat_id} |
1226 | if one_time_password is not None: |
1227 | data['otp'] = one_time_password |
1228 | @@ -56,9 +53,9 @@ def get_sso_discharge(email, password, caveat_id, one_time_password=None): |
1229 | return resp.json()['discharge_macaroon'] |
1230 | |
1231 | |
1232 | -def refresh_sso_discharge(unbound_discharge_raw): |
1233 | - sso_root = config.read_config()['services']['sso'] |
1234 | - refresh_url = urllib.parse.urljoin(sso_root, '/api/v2/tokens/refresh') |
1235 | +def refresh_sso_discharge(store, unbound_discharge_raw): |
1236 | + refresh_url = urllib.parse.urljoin( |
1237 | + store.get('sso_url'), '/api/v2/tokens/refresh') |
1238 | data = {'discharge_macaroon': unbound_discharge_raw} |
1239 | resp = requests.post( |
1240 | refresh_url, headers={'Accept': 'application/json'}, json=data) |
1241 | @@ -78,12 +75,11 @@ def _deserialize_macaroon(name, value): |
1242 | 'failed to deserialize {} macaroon'.format(name)) |
1243 | |
1244 | |
1245 | -def _get_macaroon_auth(): |
1246 | +def _get_macaroon_auth(store): |
1247 | """Return an Authorization header containing store macaroons.""" |
1248 | - credentials = config.Credentials() |
1249 | - root_raw = credentials.get('root') |
1250 | + root_raw = store.get('root') |
1251 | root = _deserialize_macaroon('root', root_raw) |
1252 | - unbound_discharge_raw = credentials.get('unbound_discharge') |
1253 | + unbound_discharge_raw = store.get('unbound_discharge') |
1254 | unbound_discharge = _deserialize_macaroon( |
1255 | 'unbound discharge', unbound_discharge_raw) |
1256 | bound_discharge = root.prepare_for_request(unbound_discharge) |
1257 | @@ -99,27 +95,25 @@ def _raise_needs_refresh(response): |
1258 | raise exceptions.StoreMacaroonNeedsRefresh() |
1259 | |
1260 | |
1261 | -def refresh_if_necessary(func, *args, **kwargs): |
1262 | +def refresh_if_necessary(store, func, *args, **kwargs): |
1263 | """Make a request, refreshing macaroons if necessary.""" |
1264 | try: |
1265 | return func(*args, **kwargs) |
1266 | except exceptions.StoreMacaroonNeedsRefresh: |
1267 | - credentials = config.Credentials() |
1268 | unbound_discharge = refresh_sso_discharge( |
1269 | - credentials.get('unbound_discharge')) |
1270 | - credentials.set('unbound_discharge', unbound_discharge) |
1271 | - credentials.save() |
1272 | + store.get('sso_url'), store.get('unbound_discharge')) |
1273 | + store.set('unbound_discharge', unbound_discharge) |
1274 | + store.save() |
1275 | return func(*args, **kwargs) |
1276 | |
1277 | |
1278 | -def get_overrides(snap_name, series='16'): |
1279 | +def get_overrides(store, snap_name, series='16'): |
1280 | """Get all overrides for a snap.""" |
1281 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
1282 | overrides_url = urllib.parse.urljoin( |
1283 | - devicegw_root, |
1284 | + store.get('gw_url'), |
1285 | '/v2/metadata/overrides/{}'.format(urllib.parse.quote_plus(snap_name))) |
1286 | headers = { |
1287 | - 'Authorization': _get_macaroon_auth(), |
1288 | + 'Authorization': _get_macaroon_auth(store), |
1289 | 'X-Ubuntu-Series': series, |
1290 | } |
1291 | resp = requests.get(overrides_url, headers=headers) |
1292 | @@ -130,12 +124,11 @@ def get_overrides(snap_name, series='16'): |
1293 | return resp.json() |
1294 | |
1295 | |
1296 | -def set_overrides(overrides): |
1297 | +def set_overrides(store, overrides): |
1298 | """Add or remove channel map overrides for a snap.""" |
1299 | - devicegw_root = config.read_config()['services']['snapdevicegw'] |
1300 | overrides_url = urllib.parse.urljoin( |
1301 | - devicegw_root, '/v2/metadata/overrides') |
1302 | - headers = {'Authorization': _get_macaroon_auth()} |
1303 | + store.get('gw_url'), '/v2/metadata/overrides') |
1304 | + headers = {'Authorization': _get_macaroon_auth(store)} |
1305 | resp = requests.post(overrides_url, headers=headers, json=overrides) |
1306 | _raise_needs_refresh(resp) |
1307 | if resp.status_code != 200: |
Project setup failed /jenkins. ols.canonical. com/online- services/ job/snapstore- client/ 3/
https:/