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