Merge ~woutervb/snapstore-client:SN-399_use_click into snapstore-client:master
- Git
- lp:~woutervb/snapstore-client
- SN-399_use_click
- Merge into master
Status: | Rejected |
---|---|
Rejected by: | Wouter van Bommel |
Proposed branch: | ~woutervb/snapstore-client:SN-399_use_click |
Merge into: | snapstore-client:master |
Diff against target: |
3881 lines (+1445/-1151) 26 files modified
.flake8 (+3/-0) .gitignore (+2/-1) Makefile (+10/-3) dev/null (+0/-65) requirements-dev.txt (+1/-0) requirements.txt (+2/-0) setup.py (+17/-0) snap/snapcraft.yaml (+9/-19) snapstore_client/cli.py (+138/-61) snapstore_client/config.py (+6/-12) snapstore_client/exceptions.py (+7/-8) snapstore_client/logic/login.py (+27/-31) snapstore_client/logic/overrides.py (+39/-39) snapstore_client/logic/push.py (+119/-111) snapstore_client/logic/tests/test_login.py (+260/-184) snapstore_client/logic/tests/test_overrides.py (+301/-230) snapstore_client/logic/tests/test_push.py (+95/-81) snapstore_client/presentation_helpers.py (+9/-9) snapstore_client/tests/factory.py (+18/-13) snapstore_client/tests/matchers.py (+8/-8) snapstore_client/tests/test_config.py (+10/-6) snapstore_client/tests/test_presentation_helpers.py (+97/-67) snapstore_client/tests/test_webservices.py (+176/-115) snapstore_client/tests/testfixtures.py (+23/-22) snapstore_client/utils.py (+9/-7) snapstore_client/webservices.py (+59/-59) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu One hackers | Pending | ||
Review via email: mp+414340@code.launchpad.net |
Commit message
CLI options moved to click / blackified
* Converted the project to a standard setuptools one
* Updated to snapcraft.yaml to make use of this
* Fixed a bug in the Makefile, preventing the creation of a snap
* Moved all commandline parsing and log handling to click & click_logging
* Updated called methods to reflex this change
* Reworked test to handle new logger
Fixes SN-399
Description of the change
Depends on updates to ols-goodyear https:/
Przemysław Suliga (suligap) wrote : | # |
Maximiliano Bertacchini (maxiberta) wrote : | # |
May I suggest to move all black+formatting into a separate MP for easier reviewing? Preferrably with a first commit adding tools, black, deps, etc. then a commit with the results of a fully automated black run, then a commit with manual formatting fixes, and so on? Thanks!
Unmerged commits
- 168e30e... by Wouter van Bommel
-
Reworked the unittest to use TestCase assertLog
Reformatted all with black for consistency, added black to Makefile &
requirements - 641d0d2... by Wouter van Bommel
-
CLI options moved to click
* Converted the project to a standard setuptools one
* Updated to snapcraft.yaml to make use of this
* Fixed a bug in the Makefile, preventing the creation of a snap
* Moved all commandline parsing and log handling to click & click_logging
* Updated called methods to reflex this changeFixes SN-399
Preview Diff
1 | diff --git a/.flake8 b/.flake8 |
2 | new file mode 100644 |
3 | index 0000000..e0ea542 |
4 | --- /dev/null |
5 | +++ b/.flake8 |
6 | @@ -0,0 +1,3 @@ |
7 | +[flake8] |
8 | +max-line-length = 88 |
9 | +extend-ignore = E203 |
10 | \ No newline at end of file |
11 | diff --git a/.gitignore b/.gitignore |
12 | index 4fc1b77..fa34ab8 100644 |
13 | --- a/.gitignore |
14 | +++ b/.gitignore |
15 | @@ -9,4 +9,5 @@ prime |
16 | stage |
17 | env |
18 | ols-wheels |
19 | -*.snap |
20 | \ No newline at end of file |
21 | +*.snap |
22 | +*.egg-info/ |
23 | diff --git a/Makefile b/Makefile |
24 | index 4a0769d..4bff705 100644 |
25 | --- a/Makefile |
26 | +++ b/Makefile |
27 | @@ -3,6 +3,7 @@ ENV = $(CURDIR)/env |
28 | PYTHON3 = $(ENV)/bin/python3 |
29 | PIP = $(PYTHON3) -m pip |
30 | FLAKE8 = $(ENV)/bin/flake8 |
31 | +BLACK = $(ENV)/bin/black |
32 | |
33 | DEPENDENCY_REPO ?= lp:~ubuntuone-pqm-team/ols-goodyear/+git/wheels |
34 | OLS_WHEELS_DIR_TMP = $(CURDIR)/ols-wheels |
35 | @@ -24,11 +25,11 @@ $(ENV)/dev: $(ENV)/prod |
36 | bootstrap: $(ENV)/prod |
37 | |
38 | snap: |
39 | - snapcraft build |
40 | + snapcraft |
41 | |
42 | test: $(ENV)/dev |
43 | $(PYTHON3) -m unittest $(TESTS) 2>&1 |
44 | - $(MAKE) --silent lint |
45 | + $(MAKE) --silent black-check lint |
46 | |
47 | /snap/bin/documentation-builder: |
48 | sudo snap install documentation-builder |
49 | @@ -37,9 +38,15 @@ docs: /snap/bin/documentation-builder |
50 | documentation-builder --base-dir docs --output-path docs/build |
51 | |
52 | |
53 | -lint: |
54 | +lint: $(ENV)/dev |
55 | $(FLAKE8) $(SERVICE_PACKAGE) |
56 | |
57 | +black: $(ENV)/dev |
58 | + $(BLACK) . |
59 | + |
60 | +black-check: $(ENV)/dev |
61 | + $(BLACK) --check . |
62 | + |
63 | coverage: $(ENV)/dev |
64 | $(PYTHON3) -m coverage erase |
65 | $(PYTHON3) -m coverage run --include "$(SERVICE_PACKAGE)*" -m unittest $(TESTS) |
66 | diff --git a/requirements-dev.txt b/requirements-dev.txt |
67 | index f7b8ab9..72daeb2 100644 |
68 | --- a/requirements-dev.txt |
69 | +++ b/requirements-dev.txt |
70 | @@ -5,3 +5,4 @@ flake8 |
71 | responses |
72 | testscenarios |
73 | testtools |
74 | +black |
75 | diff --git a/requirements.txt b/requirements.txt |
76 | index 09ae15a..0203da1 100644 |
77 | --- a/requirements.txt |
78 | +++ b/requirements.txt |
79 | @@ -2,3 +2,5 @@ pymacaroons |
80 | pysha3 |
81 | pyxdg |
82 | requests |
83 | +click |
84 | +click_logging |
85 | \ No newline at end of file |
86 | diff --git a/setup.py b/setup.py |
87 | new file mode 100644 |
88 | index 0000000..778fa97 |
89 | --- /dev/null |
90 | +++ b/setup.py |
91 | @@ -0,0 +1,17 @@ |
92 | +from setuptools import find_namespace_packages, setup |
93 | + |
94 | + |
95 | +with open("requirements.txt") as f: |
96 | + install_requires = f.read().splitlines() |
97 | + |
98 | +setup( |
99 | + name="snapstore", |
100 | + version="1.1.0", |
101 | + packages=find_namespace_packages(), |
102 | + install_requires=install_requires, |
103 | + entry_points={ |
104 | + "console_scripts": [ |
105 | + "snapstore = snapstore_client.cli:cli", |
106 | + ], |
107 | + }, |
108 | +) |
109 | diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml |
110 | index 8811c64..4563239 100644 |
111 | --- a/snap/snapcraft.yaml |
112 | +++ b/snap/snapcraft.yaml |
113 | @@ -1,36 +1,26 @@ |
114 | -name: snap-store-proxy-client |
115 | +name: snapstore-client |
116 | base: core20 |
117 | -version: "1.1" |
118 | summary: Canonical snap store proxy administration client. |
119 | description: | |
120 | The Canonical snapstore client is used to manage a snap store proxy. |
121 | - |
122 | -# Defaults, but snapcraft prints ugly yellow messages without them. |
123 | confinement: strict |
124 | grade: stable |
125 | +adopt-info: source |
126 | |
127 | architectures: |
128 | - build-on: amd64 |
129 | |
130 | apps: |
131 | - snap-store-proxy-client: |
132 | - command: ./snapstore |
133 | + snapstore: |
134 | + command: bin/snapstore |
135 | plugs: |
136 | - network |
137 | |
138 | parts: |
139 | - deps: |
140 | - plugin: python |
141 | - source: . |
142 | - requirements: |
143 | - - ${SNAPCRAFT_PART_SRC}/requirements.txt |
144 | source: |
145 | - plugin: dump |
146 | + plugin: python |
147 | source: . |
148 | - stage: |
149 | - - snapstore |
150 | - - snapstore_client/ |
151 | - override-prime: | |
152 | - snapcraftctl prime |
153 | - python3 -m compileall -q -j0 snapstore_client |
154 | - |
155 | + override-build: | |
156 | + snapcraftctl build |
157 | + version="$(python3 setup.py --version)" |
158 | + snapcraftctl set-version "$version" |
159 | \ No newline at end of file |
160 | diff --git a/snapstore b/snapstore |
161 | deleted file mode 100755 |
162 | index cd67b71..0000000 |
163 | --- a/snapstore |
164 | +++ /dev/null |
165 | @@ -1,133 +0,0 @@ |
166 | -#!/usr/bin/env python3 |
167 | -# -*- coding: utf-8 -*- |
168 | -# Copyright 2017 Canonical Ltd. This software is licensed under the |
169 | -# GNU General Public License version 3 (see the file LICENSE). |
170 | - |
171 | -"""CLI utility to manage a store-in-a-box installation.""" |
172 | - |
173 | -import argparse |
174 | -import logging |
175 | -import os |
176 | -import sys |
177 | - |
178 | -from snapstore_client.cli import configure_logging |
179 | -from snapstore_client.logic.login import login |
180 | -from snapstore_client.logic.overrides import ( |
181 | - delete_override, |
182 | - list_overrides, |
183 | - override, |
184 | -) |
185 | -from snapstore_client.logic.push import push_snap |
186 | - |
187 | - |
188 | -DEFAULT_SSO_URL = 'https://login.ubuntu.com/' |
189 | - |
190 | - |
191 | -def main(): |
192 | - try: |
193 | - configure_logging() |
194 | - args = parse_args() |
195 | - return args.func(args) or 0 |
196 | - except KeyboardInterrupt: |
197 | - # use default logger as we can't guarantee that configuration |
198 | - # has completed. |
199 | - logging.error("Operation cancelled") |
200 | - return 1 |
201 | - |
202 | - |
203 | -def parse_args(): |
204 | - parser = argparse.ArgumentParser() |
205 | - subparsers = parser.add_subparsers(help='sub-command help') |
206 | - |
207 | - login_parser = subparsers.add_parser( |
208 | - 'login', help='Sign into a store.', |
209 | - formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
210 | - login_parser.add_argument('store_url', help='Store URL') |
211 | - login_parser.add_argument('email', help='Ubuntu One SSO email', nargs='?') |
212 | - login_parser.add_argument('--sso-url', help='Ubuntu One SSO URL', |
213 | - default=DEFAULT_SSO_URL) |
214 | - login_parser.add_argument('--offline', help="Use offline mode interaction", |
215 | - action='store_true') |
216 | - login_parser.set_defaults(func=login) |
217 | - |
218 | - list_overrides_parser = subparsers.add_parser( |
219 | - 'list-overrides', help='List channel map overrides.', |
220 | - ) |
221 | - list_overrides_parser.add_argument( |
222 | - '--series', default='16', |
223 | - help='The series within which to list overrides.') |
224 | - list_overrides_parser.add_argument( |
225 | - 'snap_name', |
226 | - help='The name of the snap whose channel map should be listed.') |
227 | - list_overrides_parser.add_argument( |
228 | - '--password', |
229 | - help='Password for interacting with an offline proxy', |
230 | - default=os.environ.get('SNAP_PROXY_PASSWORD') |
231 | - ) |
232 | - list_overrides_parser.set_defaults(func=list_overrides) |
233 | - |
234 | - override_parser = subparsers.add_parser( |
235 | - 'override', help='Set channel map overrides.', |
236 | - ) |
237 | - override_parser.add_argument( |
238 | - '--series', default='16', |
239 | - help='The series within which to set overrides.') |
240 | - override_parser.add_argument( |
241 | - 'snap_name', |
242 | - help='The name of the snap whose channel map should be modified.') |
243 | - override_parser.add_argument( |
244 | - 'channel_map_entries', nargs='+', metavar='channel_map_entry', |
245 | - help='A channel map override, in the form <channel>=<revision>.') |
246 | - override_parser.add_argument( |
247 | - '--password', |
248 | - help='Password for interacting with an offline proxy', |
249 | - default=os.environ.get('SNAP_PROXY_PASSWORD') |
250 | - ) |
251 | - override_parser.set_defaults(func=override) |
252 | - |
253 | - delete_override_parser = subparsers.add_parser( |
254 | - 'delete-override', help='Delete channel map overrides.', |
255 | - ) |
256 | - delete_override_parser.add_argument( |
257 | - '--series', default='16', |
258 | - help='The series within which to delete overrides.') |
259 | - delete_override_parser.add_argument( |
260 | - 'snap_name', |
261 | - help='The name of the snap whose channel map should be modified.') |
262 | - delete_override_parser.add_argument( |
263 | - 'channels', nargs='+', metavar='channel', |
264 | - help='A channel whose overrides should be deleted.') |
265 | - delete_override_parser.add_argument( |
266 | - '--password', |
267 | - help='Password for interacting with an offline proxy', |
268 | - default=os.environ.get('SNAP_PROXY_PASSWORD') |
269 | - ) |
270 | - delete_override_parser.set_defaults(func=delete_override) |
271 | - |
272 | - push_snap_parser = subparsers.add_parser( |
273 | - 'push-snap', help='push a snap to an offline proxy') |
274 | - push_snap_parser.add_argument( |
275 | - 'snap_tar_file', |
276 | - help='The .tar.gz file of a bundled downloaded snap') |
277 | - push_snap_parser.add_argument( |
278 | - '--push-channel-map', |
279 | - action='store_true', |
280 | - help="Force push of the channel map," |
281 | - " removing any existing overrides") |
282 | - push_snap_parser.add_argument( |
283 | - '--password', |
284 | - help='Password for interacting with an offline proxy', |
285 | - default=os.environ.get('SNAP_PROXY_PASSWORD') |
286 | - ) |
287 | - push_snap_parser.set_defaults(func=push_snap) |
288 | - |
289 | - if len(sys.argv) == 1: |
290 | - # Display help if no arguments are provided. |
291 | - parser.print_help() |
292 | - sys.exit(1) |
293 | - |
294 | - return parser.parse_args() |
295 | - |
296 | - |
297 | -if __name__ == '__main__': |
298 | - sys.exit(main()) |
299 | diff --git a/snapstore_client/cli.py b/snapstore_client/cli.py |
300 | index 08008d0..f20c91b 100644 |
301 | --- a/snapstore_client/cli.py |
302 | +++ b/snapstore_client/cli.py |
303 | @@ -1,67 +1,144 @@ |
304 | -# Copyright 2017 Canonical Ltd. This software is licensed under the |
305 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
306 | # GNU General Public License version 3 (see the file LICENSE). |
307 | |
308 | """Command-line interface niceties for this service.""" |
309 | |
310 | import logging |
311 | -import os |
312 | -import sys |
313 | |
314 | - |
315 | -class _StdoutFilter(logging.Filter): |
316 | - |
317 | - def filter(self, record): |
318 | - return record.levelno <= logging.WARNING |
319 | - |
320 | - |
321 | -class _StderrFilter(logging.Filter): |
322 | - |
323 | - def filter(self, record): |
324 | - return record.levelno >= logging.ERROR |
325 | - |
326 | - |
327 | -def _is_dumb_terminal(): |
328 | - """Return True if on a dumb terminal.""" |
329 | - is_stdout_tty = os.isatty(sys.stdout.fileno()) |
330 | - is_term_dumb = os.environ.get('TERM', '') == 'dumb' |
331 | - return not is_stdout_tty or is_term_dumb |
332 | - |
333 | - |
334 | -class _ColouredFormatter(logging.Formatter): |
335 | - |
336 | - _reset = '\033[0m' |
337 | - _level_colours = { |
338 | - 'ERROR': '\033[0;31m', # Dark red |
339 | - } |
340 | - |
341 | - def format(self, record): |
342 | - colour = self._level_colours.get(record.levelname) |
343 | - log_message = super().format(record) |
344 | - if colour is not None: |
345 | - return colour + log_message + self._reset |
346 | - else: |
347 | - return log_message |
348 | - |
349 | - |
350 | -def configure_logging(logger_name=None, log_level=logging.INFO): |
351 | - stdout_handler = logging.StreamHandler(stream=sys.stdout) |
352 | - stdout_handler.addFilter(_StdoutFilter()) |
353 | - stderr_handler = logging.StreamHandler(stream=sys.stderr) |
354 | - stderr_handler.addFilter(_StderrFilter()) |
355 | - handlers = [stdout_handler, stderr_handler] |
356 | - if _is_dumb_terminal(): |
357 | - formatter = logging.Formatter(style='{') |
358 | - else: |
359 | - formatter = _ColouredFormatter(style='{') |
360 | - logger = logging.getLogger(logger_name) |
361 | - for handler in handlers: |
362 | - handler.setFormatter(formatter) |
363 | - logger.addHandler(handler) |
364 | - logger.setLevel(log_level) |
365 | - |
366 | - # The requests library is too noisy at INFO level. |
367 | - if log_level == logging.DEBUG: |
368 | - logging.getLogger('requests').setLevel(log_level) |
369 | - else: |
370 | - logging.getLogger('requests').setLevel( |
371 | - max(logging.WARNING, log_level)) |
372 | +logger = logging.getLogger(__name__) |
373 | + |
374 | +import click # noqa: E402,E501 |
375 | +import click_logging # noqa: E402,E501 |
376 | + |
377 | +from snapstore_client.logic import login as logic_login # noqa: E402,E501 |
378 | +from snapstore_client.logic.overrides import ( # noqa: E402,E501 |
379 | + list_overrides as logic_list_overrides, |
380 | +) |
381 | +from snapstore_client.logic.overrides import ( # noqa: E402,E501 |
382 | + override as logic_override, |
383 | +) |
384 | +from snapstore_client.logic.overrides import ( # noqa: E402,E501 |
385 | + delete_override as logic_delete_override, |
386 | +) |
387 | +from snapstore_client.logic import push # noqa: E402,E501 |
388 | + |
389 | + |
390 | +click_logging.basic_config(logger) |
391 | + |
392 | + |
393 | +DEFAULT_SSO_URL = "https://login.ubuntu.com/" |
394 | + |
395 | + |
396 | +@click.group() |
397 | +@click_logging.simple_verbosity_option(logger=logger) |
398 | +def cli(): |
399 | + """This tool can be used to manage an (offline) snapstore proxy instance. |
400 | + |
401 | + At the arguments that ask for an proxy password, it is possible to set |
402 | + this in an environment variable called 'SNAP_PROX_PASSWORD'. |
403 | + """ |
404 | + # Helper function to allow each subcommand be stand alone |
405 | + pass |
406 | + |
407 | + |
408 | +@cli.command() |
409 | +@click.argument("store_url") |
410 | +@click.argument("email", required=False) |
411 | +@click.option("--sso-url", help="Ubuntu One SSO URL", default=DEFAULT_SSO_URL) |
412 | +@click.option( |
413 | + "--offline/--no-offline", help="Use offline mode interaction", default=False |
414 | +) |
415 | +def login(store_url, email, sso_url, offline): |
416 | + """Login on a proxy |
417 | + |
418 | + login arguments: |
419 | + |
420 | + \b |
421 | + STORE_URL : The snapstore proxy url |
422 | + EMAIL : Ubuntu ONE SSO email (optional) |
423 | + """ |
424 | + logic_login.login( |
425 | + store_url=store_url, email=email, sso_url=sso_url, offline=offline |
426 | + ) |
427 | + |
428 | + |
429 | +@cli.command() |
430 | +@click.argument("snap_name") |
431 | +@click.option("--series", default=16, help="The series within which to list overrides.") |
432 | +@click.option( |
433 | + "--password", |
434 | + help="Password for interacting with an offline proxy", |
435 | + envvar="SNAP_PROX_PASSWORD", |
436 | +) |
437 | +def list_overrides(snap_name, series, password): |
438 | + """List channelmap overrides |
439 | + |
440 | + List channelmap overrides for SNAP_NAME""" |
441 | + logic_list_overrides(snap_name=snap_name, series=str(series), password=password) |
442 | + |
443 | + |
444 | +@cli.command() |
445 | +@click.argument("snap_name") |
446 | +@click.argument("channel_map_entries", nargs=-1, required=True) |
447 | +@click.option("--series", default=16, help="The series within which to list overrides.") |
448 | +@click.option( |
449 | + "--password", |
450 | + help="Password for interacting with an offline proxy", |
451 | + envvar="SNAP_PROX_PASSWORD", |
452 | +) |
453 | +def override(snap_name, channel_map_entries, series, password): |
454 | + """Set channelmap overrides |
455 | + |
456 | + Use CHANNEL_MAP_ENTRIES as SNAP_NAME overrides. |
457 | + |
458 | + CHANNEL_MAP_ENTRIES takes form of <channel>=<revision>""" |
459 | + logic_override( |
460 | + snap_name=snap_name, |
461 | + channel_map_entries=channel_map_entries, |
462 | + series=str(series), |
463 | + password=password, |
464 | + ) |
465 | + |
466 | + |
467 | +@cli.command() |
468 | +@click.argument("snap_name") |
469 | +@click.argument("channels", nargs=-1, required=True) |
470 | +@click.option("--series", default=16, help="The series within which to list overrides.") |
471 | +@click.option( |
472 | + "--password", |
473 | + help="Password for interacting with an offline proxy", |
474 | + envvar="SNAP_PROX_PASSWORD", |
475 | +) |
476 | +def delete_override(snap_name, channels, series, password): |
477 | + """Delete a channelmap override |
478 | + |
479 | + Delete overrides from SNAP_NAME for CHANNELS""" |
480 | + logic_delete_override( |
481 | + snap_name=snap_name, |
482 | + channels=channels, |
483 | + series=str(series), |
484 | + password=password, |
485 | + ) |
486 | + |
487 | + |
488 | +@cli.command() |
489 | +@click.argument("snap_tar_file") |
490 | +@click.option( |
491 | + "--push-channel-map", |
492 | + default=False, |
493 | + help="Force push of the channel map, removing any existing overrides", |
494 | +) |
495 | +@click.option( |
496 | + "--password", |
497 | + help="Password for interacting with an offline proxy", |
498 | + envvar="SNAP_PROX_PASSWORD", |
499 | +) |
500 | +def push_snap(snap_tar_file, push_channel_map, password): |
501 | + """Push tar bundle to proxy |
502 | + |
503 | + Push SNAP_TAR_FILE (a .tar.gz file) to the proxy.""" |
504 | + push.push_snap( |
505 | + snap_tar_file=snap_tar_file, |
506 | + push_channel_map=push_channel_map, |
507 | + password=password, |
508 | + ) |
509 | diff --git a/snapstore_client/config.py b/snapstore_client/config.py |
510 | index a01e975..636a3df 100644 |
511 | --- a/snapstore_client/config.py |
512 | +++ b/snapstore_client/config.py |
513 | @@ -12,32 +12,27 @@ from xdg import BaseDirectory |
514 | |
515 | class Config: |
516 | |
517 | - xdg_name = 'snap-store-proxy-client' |
518 | + xdg_name = "snap-store-proxy-client" |
519 | |
520 | def __init__(self): |
521 | self.parser = configparser.ConfigParser() |
522 | self.load() |
523 | |
524 | def load(self): |
525 | - path = BaseDirectory.load_first_config( |
526 | - self.xdg_name, 'config.ini') |
527 | + path = BaseDirectory.load_first_config(self.xdg_name, "config.ini") |
528 | if path is not None and os.path.exists(path): |
529 | self.parser.read(path) |
530 | |
531 | def save(self): |
532 | - path = os.path.join( |
533 | - BaseDirectory.save_config_path(self.xdg_name), |
534 | - 'config.ini') |
535 | + path = os.path.join(BaseDirectory.save_config_path(self.xdg_name), "config.ini") |
536 | # TODO: better to write atomically |
537 | - with open(path, 'w') as f: |
538 | + with open(path, "w") as f: |
539 | self.parser.write(f) |
540 | |
541 | def get(self, section, option): |
542 | try: |
543 | return self.parser.get(section, option) |
544 | - except (configparser.NoSectionError, |
545 | - configparser.NoOptionError, |
546 | - KeyError): |
547 | + except (configparser.NoSectionError, configparser.NoOptionError, KeyError): |
548 | return None |
549 | |
550 | def set(self, section, option, value): |
551 | @@ -49,11 +44,10 @@ class Config: |
552 | return Section(self, name) |
553 | |
554 | def store_section(self, name): |
555 | - return Section(self, 'store:'+name) |
556 | + return Section(self, "store:" + name) |
557 | |
558 | |
559 | class Section: |
560 | - |
561 | def __init__(self, config, name): |
562 | self.config = config |
563 | self.name = name |
564 | diff --git a/snapstore_client/exceptions.py b/snapstore_client/exceptions.py |
565 | index 061b5fe..d251fe5 100644 |
566 | --- a/snapstore_client/exceptions.py |
567 | +++ b/snapstore_client/exceptions.py |
568 | @@ -21,7 +21,7 @@ class ClientError(Exception): |
569 | |
570 | class InvalidCredentials(ClientError): |
571 | |
572 | - fmt = 'Invalid credentials: {message}.' |
573 | + fmt = "Invalid credentials: {message}." |
574 | |
575 | def __init__(self, message): |
576 | super().__init__(message=message) |
577 | @@ -29,7 +29,7 @@ class InvalidCredentials(ClientError): |
578 | |
579 | class InvalidStoreURL(ClientError): |
580 | |
581 | - fmt = 'Invalid store url: {message}.' |
582 | + fmt = "Invalid store url: {message}." |
583 | |
584 | def __init__(self, message): |
585 | super().__init__(message=message) |
586 | @@ -37,7 +37,7 @@ class InvalidStoreURL(ClientError): |
587 | |
588 | class StoreCommunicationError(ClientError): |
589 | |
590 | - fmt = 'Connection error with the store using: {message}.' |
591 | + fmt = "Connection error with the store using: {message}." |
592 | |
593 | def __init__(self, message): |
594 | super().__init__(message=message) |
595 | @@ -45,7 +45,7 @@ class StoreCommunicationError(ClientError): |
596 | |
597 | class StoreMacaroonSSOMismatch(ClientError): |
598 | |
599 | - fmt = 'Root macaroon does not refer to expected SSO host: {sso_host}.' |
600 | + fmt = "Root macaroon does not refer to expected SSO host: {sso_host}." |
601 | |
602 | def __init__(self, sso_host): |
603 | super().__init__(sso_host=sso_host) |
604 | @@ -55,18 +55,17 @@ class StoreAuthenticationError(ClientError): |
605 | |
606 | # No terminating full stop because the message from SSO sometimes |
607 | # (though not always!) includes one. |
608 | - fmt = 'Authentication error: {message}' |
609 | + fmt = "Authentication error: {message}" |
610 | |
611 | def __init__(self, message, extra=None): |
612 | super().__init__(message=message, extra=extra) |
613 | |
614 | |
615 | class StoreTwoFactorAuthenticationRequired(StoreAuthenticationError): |
616 | - |
617 | def __init__(self): |
618 | - super().__init__('Two-factor authentication required.') |
619 | + super().__init__("Two-factor authentication required.") |
620 | |
621 | |
622 | class StoreMacaroonNeedsRefresh(ClientError): |
623 | |
624 | - fmt = 'Authentication macaroon needs to be refreshed.' |
625 | + fmt = "Authentication macaroon needs to be refreshed." |
626 | diff --git a/snapstore_client/logic/login.py b/snapstore_client/logic/login.py |
627 | index 9550988..bc91422 100644 |
628 | --- a/snapstore_client/logic/login.py |
629 | +++ b/snapstore_client/logic/login.py |
630 | @@ -1,7 +1,6 @@ |
631 | -# Copyright 2017 Canonical Ltd. |
632 | +# Copyright 2021 Canonical Ltd. |
633 | |
634 | import getpass |
635 | -import logging |
636 | from urllib.parse import urlparse |
637 | |
638 | from pymacaroons import Macaroon |
639 | @@ -11,9 +10,7 @@ from snapstore_client import ( |
640 | exceptions, |
641 | webservices as ws, |
642 | ) |
643 | - |
644 | - |
645 | -logger = logging.getLogger(__name__) |
646 | +from snapstore_client.cli import logger |
647 | |
648 | |
649 | def _extract_caveat_id(sso_url, root_macaroon): |
650 | @@ -26,23 +23,21 @@ def _extract_caveat_id(sso_url, root_macaroon): |
651 | raise exceptions.StoreMacaroonSSOMismatch(sso_host) |
652 | |
653 | |
654 | -def login(args): |
655 | +def login(store_url, email, sso_url, offline): |
656 | # TODO: validate these before using to avoid ugly errors. |
657 | - gw_url = args.store_url |
658 | - sso_url = args.sso_url |
659 | + gw_url = store_url |
660 | |
661 | - if args.offline: |
662 | + if offline: |
663 | cfg = config.Config() |
664 | - store = cfg.store_section('default') |
665 | - store.set('gw_url', gw_url) |
666 | + store = cfg.store_section("default") |
667 | + store.set("gw_url", gw_url) |
668 | cfg.save() |
669 | return |
670 | |
671 | - logger.info('Enter your Ubuntu One SSO credentials.') |
672 | - email = args.email |
673 | + logger.info("Enter your Ubuntu One SSO credentials.") |
674 | if not email: |
675 | - email = input('Email: ') |
676 | - password = getpass.getpass('Password: ') |
677 | + email = input("Email: ") |
678 | + password = getpass.getpass("Password: ") |
679 | |
680 | try: |
681 | root = ws.issue_store_admin(gw_url) |
682 | @@ -53,22 +48,23 @@ def login(args): |
683 | try: |
684 | try: |
685 | unbound_discharge = ws.get_sso_discharge( |
686 | - sso_url, email, password, caveat_id) |
687 | - logger.info('Login successful') |
688 | + sso_url, email, password, caveat_id |
689 | + ) |
690 | + logger.info("Login successful") |
691 | except exceptions.StoreTwoFactorAuthenticationRequired: |
692 | - one_time_password = input('Second-factor auth: ') |
693 | + one_time_password = input("Second-factor auth: ") |
694 | unbound_discharge = ws.get_sso_discharge( |
695 | - sso_url, email, password, caveat_id, |
696 | - one_time_password=one_time_password) |
697 | - logger.info('Login successful') |
698 | + sso_url, email, password, caveat_id, one_time_password=one_time_password |
699 | + ) |
700 | + logger.info("Login successful") |
701 | except exceptions.StoreAuthenticationError as e: |
702 | - logger.error('Login failed.') |
703 | - logger.error('%s', e) |
704 | + logger.error("Login failed.") |
705 | + logger.error("%s", e) |
706 | if e.extra: |
707 | for key, value in e.extra.items(): |
708 | if isinstance(value, list): |
709 | - value = ' '.join(value) |
710 | - logger.error('%s: %s', key, value) |
711 | + value = " ".join(value) |
712 | + logger.error("%s: %s", key, value) |
713 | return 1 |
714 | |
715 | cfg = config.Config() |
716 | @@ -76,12 +72,12 @@ def login(args): |
717 | # to support multiple stores by allowing the user to provide a nice name |
718 | # for a store at login that can be used to select the store for later |
719 | # operations. |
720 | - store = cfg.store_section('default') |
721 | - store.set('gw_url', gw_url) |
722 | - store.set('sso_url', sso_url) |
723 | - store.set('root', root) |
724 | - store.set('unbound_discharge', unbound_discharge) |
725 | - store.set('email', email) |
726 | + store = cfg.store_section("default") |
727 | + store.set("gw_url", gw_url) |
728 | + store.set("sso_url", sso_url) |
729 | + store.set("root", root) |
730 | + store.set("unbound_discharge", unbound_discharge) |
731 | + store.set("email", email) |
732 | cfg.save() |
733 | |
734 | return 0 |
735 | diff --git a/snapstore_client/logic/overrides.py b/snapstore_client/logic/overrides.py |
736 | index d3c8356..e91a6e9 100644 |
737 | --- a/snapstore_client/logic/overrides.py |
738 | +++ b/snapstore_client/logic/overrides.py |
739 | @@ -1,6 +1,4 @@ |
740 | -# Copyright 2017 Canonical Ltd. |
741 | - |
742 | -import logging |
743 | +# Copyright 2021 Canonical Ltd. |
744 | |
745 | from requests.exceptions import HTTPError |
746 | |
747 | @@ -20,94 +18,96 @@ from snapstore_client.utils import ( |
748 | ) |
749 | |
750 | |
751 | -logger = logging.getLogger(__name__) |
752 | +from snapstore_client.cli import logger |
753 | |
754 | |
755 | -def list_overrides(args): |
756 | +def list_overrides(snap_name, series, password=None): |
757 | cfg = config.Config() |
758 | store = _check_default_store(cfg) |
759 | if not store: |
760 | return 1 |
761 | |
762 | try: |
763 | - if args.password: |
764 | + if password: |
765 | response = ws.get_overrides( |
766 | - store, args.snap_name, |
767 | - series=args.series, password=args.password) |
768 | + store, snap_name, series=series, password=password |
769 | + ) |
770 | else: |
771 | response = ws.refresh_if_necessary( |
772 | - store, ws.get_overrides, |
773 | - store, args.snap_name, series=args.series) |
774 | + store, ws.get_overrides, store, snap_name, series=series |
775 | + ) |
776 | except exceptions.InvalidCredentials as e: |
777 | _log_credentials_error(e) |
778 | return 1 |
779 | except HTTPError: |
780 | _log_authorized_error() |
781 | return 1 |
782 | - for override in response['overrides']: |
783 | + for override in response["overrides"]: |
784 | logger.info(override_to_string(override)) |
785 | |
786 | |
787 | -def override(args): |
788 | +def override(snap_name, channel_map_entries, series, password): |
789 | cfg = config.Config() |
790 | store = _check_default_store(cfg) |
791 | if not store: |
792 | return 1 |
793 | |
794 | overrides = [] |
795 | - for channel_map_entry in args.channel_map_entries: |
796 | + for channel_map_entry in channel_map_entries: |
797 | channel, revision = channel_map_string_to_tuple(channel_map_entry) |
798 | - overrides.append({ |
799 | - 'snap_name': args.snap_name, |
800 | - 'revision': revision, |
801 | - 'channel': channel, |
802 | - 'series': args.series, |
803 | - }) |
804 | + overrides.append( |
805 | + { |
806 | + "snap_name": snap_name, |
807 | + "revision": revision, |
808 | + "channel": channel, |
809 | + "series": series, |
810 | + } |
811 | + ) |
812 | try: |
813 | - if args.password: |
814 | - response = ws.set_overrides( |
815 | - store, overrides, password=args.password) |
816 | + if password: |
817 | + response = ws.set_overrides(store, overrides, password=password) |
818 | else: |
819 | response = ws.refresh_if_necessary( |
820 | - store, ws.set_overrides, |
821 | - store, overrides) |
822 | + store, ws.set_overrides, store, overrides |
823 | + ) |
824 | except exceptions.InvalidCredentials as e: |
825 | _log_credentials_error(e) |
826 | return 1 |
827 | except HTTPError: |
828 | _log_authorized_error() |
829 | return 1 |
830 | - for override in response['overrides']: |
831 | + for override in response["overrides"]: |
832 | logger.info(override_to_string(override)) |
833 | |
834 | |
835 | -def delete_override(args): |
836 | +def delete_override(snap_name, channels, series, password): |
837 | cfg = config.Config() |
838 | store = _check_default_store(cfg) |
839 | if not store: |
840 | return 1 |
841 | |
842 | overrides = [] |
843 | - for channel in args.channels: |
844 | - overrides.append({ |
845 | - 'snap_name': args.snap_name, |
846 | - 'revision': None, |
847 | - 'channel': channel, |
848 | - 'series': args.series, |
849 | - }) |
850 | + for channel in channels: |
851 | + overrides.append( |
852 | + { |
853 | + "snap_name": snap_name, |
854 | + "revision": None, |
855 | + "channel": channel, |
856 | + "series": series, |
857 | + } |
858 | + ) |
859 | try: |
860 | - if args.password: |
861 | - response = ws.set_overrides( |
862 | - store, overrides, password=args.password) |
863 | + if password: |
864 | + response = ws.set_overrides(store, overrides, password=password) |
865 | else: |
866 | response = ws.refresh_if_necessary( |
867 | - store, ws.set_overrides, |
868 | - store, overrides) |
869 | + store, ws.set_overrides, store, overrides |
870 | + ) |
871 | except exceptions.InvalidCredentials as e: |
872 | _log_credentials_error(e) |
873 | return 1 |
874 | except HTTPError: |
875 | _log_authorized_error() |
876 | return 1 |
877 | - for override in response['overrides']: |
878 | + for override in response["overrides"]: |
879 | logger.info(override_to_string(override)) |
880 | diff --git a/snapstore_client/logic/push.py b/snapstore_client/logic/push.py |
881 | index 1a91bbb..315b46d 100644 |
882 | --- a/snapstore_client/logic/push.py |
883 | +++ b/snapstore_client/logic/push.py |
884 | @@ -1,3 +1,5 @@ |
885 | +# Copyright 2021 Canonical Ltd. |
886 | + |
887 | import json |
888 | import logging |
889 | from pathlib import Path |
890 | @@ -22,7 +24,7 @@ logger = logging.getLogger(__name__) |
891 | |
892 | |
893 | # XXX twom 2019-03-19 hardcoded for now, awaiting RBAC integration |
894 | -USERNAME = 'admin' |
895 | +USERNAME = "admin" |
896 | |
897 | |
898 | class pushException(Exception): |
899 | @@ -36,46 +38,46 @@ class ChannelMapExistsException(Exception): |
900 | def _push_ident(store, password, downloaded_map): |
901 | """push the snap details to snapident""" |
902 | |
903 | - snap_id = downloaded_map['snap-id'] |
904 | - snap_details = downloaded_map['snap'] |
905 | + snap_id = downloaded_map["snap-id"] |
906 | + snap_details = downloaded_map["snap"] |
907 | push_details = { |
908 | - 'snap_id': snap_id, |
909 | - 'package_type': 'snap', |
910 | - 'snap_name': snap_details['name'], |
911 | - 'private': False, # If you're pushing to your own proxy |
912 | - 'stores': ['ubuntu'], # ditto |
913 | - 'blob': snap_details, |
914 | - 'publisher_id': snap_details['publisher']['id'], |
915 | - 'publisher_name': snap_details['publisher']['display-name'], |
916 | - 'publisher_title': snap_details['publisher']['username'], |
917 | - 'publisher_validation': snap_details['publisher']['validation'], |
918 | - 'license': snap_details['license'], |
919 | - 'prices': snap_details['prices'], |
920 | - 'summary': snap_details['summary'], |
921 | - 'title': snap_details['title'], |
922 | + "snap_id": snap_id, |
923 | + "package_type": "snap", |
924 | + "snap_name": snap_details["name"], |
925 | + "private": False, # If you're pushing to your own proxy |
926 | + "stores": ["ubuntu"], # ditto |
927 | + "blob": snap_details, |
928 | + "publisher_id": snap_details["publisher"]["id"], |
929 | + "publisher_name": snap_details["publisher"]["display-name"], |
930 | + "publisher_title": snap_details["publisher"]["username"], |
931 | + "publisher_validation": snap_details["publisher"]["validation"], |
932 | + "license": snap_details["license"], |
933 | + "prices": snap_details["prices"], |
934 | + "summary": snap_details["summary"], |
935 | + "title": snap_details["title"], |
936 | } |
937 | |
938 | - url = store.get('gw_url') |
939 | + url = store.get("gw_url") |
940 | |
941 | response = requests.post( |
942 | - url + '/snaps/update', |
943 | - json={'snaps': [push_details]}, |
944 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password) |
945 | + url + "/snaps/update", |
946 | + json={"snaps": [push_details]}, |
947 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
948 | ) |
949 | if response.status_code != 200: |
950 | raise pushException( |
951 | - "Failure in pushing to snapident: {}".format( |
952 | - response.content)) |
953 | + "Failure in pushing to snapident: {}".format(response.content) |
954 | + ) |
955 | |
956 | |
957 | def _push_revs(store, password, downloaded_map): |
958 | """push the revision information to snaprevs""" |
959 | - store_url = store.get('gw_url') |
960 | + store_url = store.get("gw_url") |
961 | |
962 | - snap_id = downloaded_map['snap-id'] |
963 | - for instance in downloaded_map['channel-map']: |
964 | + snap_id = downloaded_map["snap-id"] |
965 | + for instance in downloaded_map["channel-map"]: |
966 | # rewrite the download_url |
967 | - download_url = instance['download']['url'] |
968 | + download_url = instance["download"]["url"] |
969 | |
970 | url = urlsplit(download_url) |
971 | fqdn = urlsplit(store_url) |
972 | @@ -84,90 +86,93 @@ def _push_revs(store, password, downloaded_map): |
973 | |
974 | # create the revision |
975 | rev = { |
976 | - 'snap_id': snap_id, |
977 | - 'package_type': 'snap', |
978 | - 'revision': instance['revision'], |
979 | - 'version': instance['version'], |
980 | - 'confinement': instance['confinement'], |
981 | - 'architectures': instance['architectures'], |
982 | - 'created_at': instance['created-at'], |
983 | + "snap_id": snap_id, |
984 | + "package_type": "snap", |
985 | + "revision": instance["revision"], |
986 | + "version": instance["version"], |
987 | + "confinement": instance["confinement"], |
988 | + "architectures": instance["architectures"], |
989 | + "created_at": instance["created-at"], |
990 | # This is required, but isn't available to retrieve from /info |
991 | - 'created_by': '', |
992 | - 'binary_path': download_url, |
993 | - 'binary_filesize': instance['download']['size'], |
994 | - 'binary_sha3_384': instance['download']['sha3-384'], |
995 | - 'snap_yaml': instance.get('snap-yaml', ''), |
996 | - 'epoch': instance['epoch'], |
997 | - 'type': instance['type'], |
998 | - 'base': instance['base'], |
999 | - 'common_ids': instance['common-ids'], |
1000 | + "created_by": "", |
1001 | + "binary_path": download_url, |
1002 | + "binary_filesize": instance["download"]["size"], |
1003 | + "binary_sha3_384": instance["download"]["sha3-384"], |
1004 | + "snap_yaml": instance.get("snap-yaml", ""), |
1005 | + "epoch": instance["epoch"], |
1006 | + "type": instance["type"], |
1007 | + "base": instance["base"], |
1008 | + "common_ids": instance["common-ids"], |
1009 | } |
1010 | revs_response = requests.post( |
1011 | - store_url + '/revisions/create', |
1012 | + store_url + "/revisions/create", |
1013 | json=[rev], |
1014 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password) |
1015 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
1016 | ) |
1017 | # 409 / Conflict - already exists |
1018 | if revs_response.status_code not in [201, 409]: |
1019 | raise pushException( |
1020 | "Failure to push revisions to snaprevs: {}".format( |
1021 | - revs_response.content)) |
1022 | + revs_response.content |
1023 | + ) |
1024 | + ) |
1025 | |
1026 | |
1027 | def _push_channelmap(store, password, downloaded_map, force_push=False): |
1028 | """push the channel map to snaprevs""" |
1029 | - store_url = store.get('gw_url') |
1030 | + store_url = store.get("gw_url") |
1031 | |
1032 | - snap_id = downloaded_map['snap-id'] |
1033 | + snap_id = downloaded_map["snap-id"] |
1034 | |
1035 | filter_payload = { |
1036 | - 'filters': [ |
1037 | + "filters": [ |
1038 | { |
1039 | - 'series': '16', |
1040 | - 'package_type': 'snap', |
1041 | - 'snap_id': snap_id, |
1042 | + "series": "16", |
1043 | + "package_type": "snap", |
1044 | + "snap_id": snap_id, |
1045 | }, |
1046 | ], |
1047 | } |
1048 | # Check if we have an existing channel map |
1049 | existing = requests.post( |
1050 | - store_url + '/channelmaps/filter', |
1051 | + store_url + "/channelmaps/filter", |
1052 | json=filter_payload, |
1053 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password)) |
1054 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
1055 | + ) |
1056 | if existing.status_code != 200: |
1057 | raise pushException("Error retrieving current channelmap") |
1058 | - if existing.json().get('channelmaps') and not force_push: |
1059 | + if existing.json().get("channelmaps") and not force_push: |
1060 | raise ChannelMapExistsException("Channel map already exists.") |
1061 | |
1062 | - for instance in downloaded_map['channel-map']: |
1063 | - if instance['channel']['track'] == 'latest': |
1064 | + for instance in downloaded_map["channel-map"]: |
1065 | + if instance["channel"]["track"] == "latest": |
1066 | track = None |
1067 | else: |
1068 | - track = instance['channel']['track'] |
1069 | + track = instance["channel"]["track"] |
1070 | chan_map = { |
1071 | # This is wrong, it should be the current developer id |
1072 | # but the pusher might not be a developer |
1073 | # XXX (twom): can we get this from upstream? |
1074 | - 'developer_id': downloaded_map['snap']['publisher']['id'], |
1075 | - 'release_requests': [ |
1076 | + "developer_id": downloaded_map["snap"]["publisher"]["id"], |
1077 | + "release_requests": [ |
1078 | { |
1079 | - 'package_type': 'snap', |
1080 | - 'snap_id': snap_id, |
1081 | - 'channel': [ |
1082 | + "package_type": "snap", |
1083 | + "snap_id": snap_id, |
1084 | + "channel": [ |
1085 | track, |
1086 | - instance['channel']['risk'], |
1087 | + instance["channel"]["risk"], |
1088 | None, # We can't deal in 'branch', it's not on the API |
1089 | ], |
1090 | - 'architecture': instance['channel']['architecture'], |
1091 | - 'series': '16', |
1092 | - 'revision': instance['revision'] |
1093 | + "architecture": instance["channel"]["architecture"], |
1094 | + "series": "16", |
1095 | + "revision": instance["revision"], |
1096 | } |
1097 | - ] |
1098 | + ], |
1099 | } |
1100 | map_response = requests.post( |
1101 | - store_url + '/channelmaps/update', |
1102 | + store_url + "/channelmaps/update", |
1103 | json=chan_map, |
1104 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password) |
1105 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
1106 | ) |
1107 | if map_response.status_code != 200: |
1108 | raise pushException("Error pushing channel map") |
1109 | @@ -185,7 +190,7 @@ def _split_assertions(raw_contents): |
1110 | current_assert = [] |
1111 | for line in raw_contents: |
1112 | # if we've encountered a new assert, save the old one, start a new one |
1113 | - if line.startswith('type: ') and current_assert: |
1114 | + if line.startswith("type: ") and current_assert: |
1115 | available_asserts.append(current_assert) |
1116 | current_assert = [] |
1117 | current_assert.append(line) |
1118 | @@ -196,33 +201,34 @@ def _split_assertions(raw_contents): |
1119 | # from between the concatenated asserts. |
1120 | # Remove it from the ones that have it so they're clean to push |
1121 | for assertion in available_asserts: |
1122 | - assertion[:] = assertion[:-1] if assertion[-1] == '' else assertion |
1123 | + assertion[:] = assertion[:-1] if assertion[-1] == "" else assertion |
1124 | return available_asserts |
1125 | |
1126 | |
1127 | def _add_assertion_to_service(store, password, list_assertions): |
1128 | - store_url = store.get('gw_url') |
1129 | + store_url = store.get("gw_url") |
1130 | |
1131 | # push the split asserts |
1132 | for assertion in list_assertions: |
1133 | # get the type |
1134 | - assert_type = assertion[0].split('type: ')[1] |
1135 | + assert_type = assertion[0].split("type: ")[1] |
1136 | # ignore the account-key of canonical, it's already a trusted assert |
1137 | # and will error on push |
1138 | - canonical_key = 'account-id: canonical' |
1139 | - if (assert_type == 'account-key' and canonical_key in assertion): |
1140 | + canonical_key = "account-id: canonical" |
1141 | + if assert_type == "account-key" and canonical_key in assertion: |
1142 | continue |
1143 | - if (assert_type == 'account' and canonical_key in assertion): |
1144 | + if assert_type == "account" and canonical_key in assertion: |
1145 | continue |
1146 | response = requests.post( |
1147 | - store_url + '/v1/assertions', |
1148 | - '\n'.join(assertion).encode('utf-8'), |
1149 | - headers={'Content-Type': 'application/x.ubuntu.assertion'}, |
1150 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password)) |
1151 | + store_url + "/v1/assertions", |
1152 | + "\n".join(assertion).encode("utf-8"), |
1153 | + headers={"Content-Type": "application/x.ubuntu.assertion"}, |
1154 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
1155 | + ) |
1156 | if response.status_code != 201: |
1157 | raise pushException( |
1158 | - "Failed to push: {} - {}".format( |
1159 | - response.status_code, response.content)) |
1160 | + "Failed to push: {} - {}".format(response.status_code, response.content) |
1161 | + ) |
1162 | |
1163 | |
1164 | def _push_assertions(store, password, assert_path): |
1165 | @@ -241,13 +247,14 @@ def _push_assertions(store, password, assert_path): |
1166 | |
1167 | def _push_file_to_nginx_cache(store, password, snap_path, snap_id, revision): |
1168 | """Add the file to the nginx cache via the snapproxy service""" |
1169 | - store_url = store.get('gw_url') |
1170 | - target_filename = '{}_{}.snap'.format(snap_id, revision) |
1171 | - files = {target_filename: open(snap_path, 'rb')} |
1172 | + store_url = store.get("gw_url") |
1173 | + target_filename = "{}_{}.snap".format(snap_id, revision) |
1174 | + files = {target_filename: open(snap_path, "rb")} |
1175 | response = requests.post( |
1176 | - store_url + '/files/upload', |
1177 | + store_url + "/files/upload", |
1178 | files=files, |
1179 | - auth=requests.auth.HTTPBasicAuth(USERNAME, password)) |
1180 | + auth=requests.auth.HTTPBasicAuth(USERNAME, password), |
1181 | + ) |
1182 | if response.status_code != 200: |
1183 | print(response.status_code) |
1184 | raise pushException("Failed to push file to proxy cache") |
1185 | @@ -259,10 +266,10 @@ def _push(store, tar_file, password, force_channel_map=False): |
1186 | to an offline proxy |
1187 | """ |
1188 | extract_dir = Path(tempfile.mkdtemp()) |
1189 | - with tarfile.open(str(tar_file), 'r:gz') as tar: |
1190 | + with tarfile.open(str(tar_file), "r:gz") as tar: |
1191 | tar.extractall(str(extract_dir)) |
1192 | |
1193 | - json_file = extract_dir / 'channel-map.json' |
1194 | + json_file = extract_dir / "channel-map.json" |
1195 | json_path = Path(json_file) |
1196 | if not json_path.exists(): |
1197 | logger.error("Snap information file not found at {}".format(json_path)) |
1198 | @@ -274,53 +281,54 @@ def _push(store, tar_file, password, force_channel_map=False): |
1199 | _push_ident(store, password, downloaded_map) |
1200 | _push_revs(store, password, downloaded_map) |
1201 | try: |
1202 | - _push_channelmap( |
1203 | - store, password, downloaded_map, force_channel_map) |
1204 | + _push_channelmap(store, password, downloaded_map, force_channel_map) |
1205 | except ChannelMapExistsException as e: |
1206 | logger.info(str(e)) |
1207 | logger.info( |
1208 | "Not updating the channel map, either manage revisions using " |
1209 | - "`snap-proxy override` or try again with `--push-channel-map`") |
1210 | - |
1211 | - for instance in downloaded_map['channel-map']: |
1212 | - snap_name = downloaded_map['snap']['name'] |
1213 | - snap_file_name = '{}_{}.snap'.format( |
1214 | - snap_name, instance['revision']) |
1215 | - assert_file_name = '{}_{}.assert'.format( |
1216 | - snap_name, instance['revision']) |
1217 | + "`snap-proxy override` or try again with `--push-channel-map`" |
1218 | + ) |
1219 | + |
1220 | + for instance in downloaded_map["channel-map"]: |
1221 | + snap_name = downloaded_map["snap"]["name"] |
1222 | + snap_file_name = "{}_{}.snap".format(snap_name, instance["revision"]) |
1223 | + assert_file_name = "{}_{}.assert".format(snap_name, instance["revision"]) |
1224 | snap_path = str(json_path.parent / snap_file_name) |
1225 | assert_path = str(json_path.parent / assert_file_name) |
1226 | try: |
1227 | _push_file_to_nginx_cache( |
1228 | - store, password, snap_path, |
1229 | - downloaded_map['snap-id'], instance['revision'], |
1230 | + store, |
1231 | + password, |
1232 | + snap_path, |
1233 | + downloaded_map["snap-id"], |
1234 | + instance["revision"], |
1235 | ) |
1236 | except FileNotFoundError as exc: |
1237 | logger.warning( |
1238 | - 'Skipping revision %s (%s/%s) as %s not found', |
1239 | - instance['revision'], |
1240 | - instance['channel']['name'], |
1241 | - instance['channel']['architecture'], |
1242 | + "Skipping revision %s (%s/%s) as %s not found", |
1243 | + instance["revision"], |
1244 | + instance["channel"]["name"], |
1245 | + instance["channel"]["architecture"], |
1246 | exc.filename, |
1247 | ) |
1248 | else: |
1249 | _push_assertions(store, password, assert_path) |
1250 | |
1251 | |
1252 | -def push_snap(args): |
1253 | +def push_snap(snap_tar_file, push_channel_map, password): |
1254 | cfg = config.Config() |
1255 | store = _check_default_store(cfg) |
1256 | if not store: |
1257 | return 1 |
1258 | |
1259 | try: |
1260 | - if args.password: |
1261 | - _push(store, args.snap_tar_file, |
1262 | - args.password, args.push_channel_map) |
1263 | + if password: |
1264 | + _push(store, snap_tar_file, password, push_channel_map) |
1265 | else: |
1266 | logger.error( |
1267 | "This command only works with a supplied password" |
1268 | - " and a proxy in offline mode") |
1269 | + " and a proxy in offline mode" |
1270 | + ) |
1271 | except exceptions.InvalidCredentials as e: |
1272 | _log_credentials_error(e) |
1273 | return 1 |
1274 | diff --git a/snapstore_client/logic/tests/test_login.py b/snapstore_client/logic/tests/test_login.py |
1275 | index d7854cc..a4a0fcc 100644 |
1276 | --- a/snapstore_client/logic/tests/test_login.py |
1277 | +++ b/snapstore_client/logic/tests/test_login.py |
1278 | @@ -19,268 +19,344 @@ from snapstore_client import ( |
1279 | exceptions, |
1280 | ) |
1281 | from snapstore_client.logic.login import login |
1282 | -from snapstore_client.tests import factory |
1283 | |
1284 | |
1285 | class LoginTests(TestCase): |
1286 | - |
1287 | def setUp(self): |
1288 | super().setUp() |
1289 | - self.default_gw_url = 'http://store.local/' |
1290 | - self.default_sso_url = 'https://login.staging.ubuntu.com/' |
1291 | - self.logger = self.useFixture(fixtures.FakeLogger()) |
1292 | + self.default_gw_url = "http://store.local/" |
1293 | + self.default_sso_url = "https://login.staging.ubuntu.com/" |
1294 | self.config_path = self.useFixture(fixtures.TempDir()).path |
1295 | - self.useFixture(fixtures.MonkeyPatch( |
1296 | - 'xdg.BaseDirectory.xdg_config_home', self.config_path)) |
1297 | - self.useFixture(fixtures.MonkeyPatch( |
1298 | - 'xdg.BaseDirectory.xdg_config_dirs', [self.config_path])) |
1299 | - self.mock_input = self.useFixture(fixtures.MockPatch( |
1300 | - 'builtins.input')).mock |
1301 | - self.mock_getpass = self.useFixture(fixtures.MockPatch( |
1302 | - 'getpass.getpass')).mock |
1303 | + self.useFixture( |
1304 | + fixtures.MonkeyPatch("xdg.BaseDirectory.xdg_config_home", self.config_path) |
1305 | + ) |
1306 | + self.useFixture( |
1307 | + fixtures.MonkeyPatch( |
1308 | + "xdg.BaseDirectory.xdg_config_dirs", [self.config_path] |
1309 | + ) |
1310 | + ) |
1311 | + self.mock_input = self.useFixture(fixtures.MockPatch("builtins.input")).mock |
1312 | + self.mock_getpass = self.useFixture(fixtures.MockPatch("getpass.getpass")).mock |
1313 | |
1314 | def make_responses_callback(self, response_templates): |
1315 | full_responses = [] |
1316 | for response in response_templates: |
1317 | - status = response.get('status', 200) |
1318 | - content_type = 'text/plain' |
1319 | - if 'json' in response: |
1320 | - content_type = 'application/json' |
1321 | - body = json.dumps(response['json']) |
1322 | + status = response.get("status", 200) |
1323 | + content_type = "text/plain" |
1324 | + if "json" in response: |
1325 | + content_type = "application/json" |
1326 | + body = json.dumps(response["json"]) |
1327 | else: |
1328 | - body = response.get('body') |
1329 | - full_responses.append( |
1330 | - (status, {'Content-Type': content_type}, body)) |
1331 | + body = response.get("body") |
1332 | + full_responses.append((status, {"Content-Type": content_type}, body)) |
1333 | iter_responses = iter(full_responses) |
1334 | return lambda request: next(iter_responses) |
1335 | |
1336 | - def make_args(self, store_url=None, |
1337 | - sso_url=None, email=None, offline=False): |
1338 | - return factory.Args( |
1339 | - store_url=store_url or self.default_gw_url, |
1340 | - sso_url=sso_url or self.default_sso_url, |
1341 | - email=email, |
1342 | - offline=offline, |
1343 | - ) |
1344 | + def make_args(self, store_url=None, sso_url=None, email=None, offline=False): |
1345 | + return { |
1346 | + "store_url": store_url or self.default_gw_url, |
1347 | + "sso_url": sso_url or self.default_sso_url, |
1348 | + "email": email, |
1349 | + "offline": offline, |
1350 | + } |
1351 | |
1352 | def add_issue_store_admin_response(self, *response_templates, gw_url=None): |
1353 | gw_url = gw_url or self.default_gw_url |
1354 | - issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
1355 | + issue_store_admin_url = urljoin(gw_url, "/v2/auth/issue-store-admin") |
1356 | responses.add_callback( |
1357 | - 'POST', issue_store_admin_url, |
1358 | - self.make_responses_callback(response_templates)) |
1359 | + "POST", |
1360 | + issue_store_admin_url, |
1361 | + self.make_responses_callback(response_templates), |
1362 | + ) |
1363 | |
1364 | - def add_get_sso_discharge_response(self, *response_templates, |
1365 | - sso_url=None): |
1366 | + def add_get_sso_discharge_response(self, *response_templates, sso_url=None): |
1367 | sso_url = sso_url or self.default_sso_url |
1368 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
1369 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
1370 | responses.add_callback( |
1371 | - 'POST', discharge_url, |
1372 | - self.make_responses_callback(response_templates)) |
1373 | + "POST", discharge_url, self.make_responses_callback(response_templates) |
1374 | + ) |
1375 | |
1376 | def make_root_macaroon(self, sso_url=None): |
1377 | macaroon = Macaroon() |
1378 | sso_url = sso_url or self.default_sso_url |
1379 | sso_host = urlparse(sso_url).netloc |
1380 | - macaroon.add_third_party_caveat(sso_host, 'key', 'payload') |
1381 | + macaroon.add_third_party_caveat(sso_host, "key", "payload") |
1382 | return macaroon.serialize() |
1383 | |
1384 | @responses.activate |
1385 | def test_login_sso_mismatch(self): |
1386 | - self.mock_input.return_value = 'user@example.org' |
1387 | - self.mock_getpass.return_value = 'secret' |
1388 | + self.mock_input.return_value = "user@example.org" |
1389 | + self.mock_getpass.return_value = "secret" |
1390 | macaroon = Macaroon() |
1391 | - macaroon.add_third_party_caveat('another.example.com', '', '') |
1392 | + macaroon.add_third_party_caveat("another.example.com", "", "") |
1393 | self.add_issue_store_admin_response( |
1394 | - {'status': 200, 'json': {'macaroon': macaroon.serialize()}}) |
1395 | - self.add_get_sso_discharge_response({'status': 401}) |
1396 | + {"status": 200, "json": {"macaroon": macaroon.serialize()}} |
1397 | + ) |
1398 | + self.add_get_sso_discharge_response({"status": 401}) |
1399 | self.assertRaises( |
1400 | - exceptions.StoreMacaroonSSOMismatch, login, self.make_args()) |
1401 | + exceptions.StoreMacaroonSSOMismatch, login, **self.make_args() |
1402 | + ) |
1403 | |
1404 | @responses.activate |
1405 | def test_login_sso_bad_email(self): |
1406 | - self.mock_input.return_value = '' |
1407 | - self.mock_getpass.return_value = '' |
1408 | + self.mock_input.return_value = "" |
1409 | + self.mock_getpass.return_value = "" |
1410 | self.add_issue_store_admin_response( |
1411 | - {'status': 200, 'json': {'macaroon': self.make_root_macaroon()}}) |
1412 | + {"status": 200, "json": {"macaroon": self.make_root_macaroon()}} |
1413 | + ) |
1414 | auth_error = { |
1415 | - 'message': 'Invalid request data', |
1416 | - 'extra': {'email': ['Enter a valid email address.']}, |
1417 | + "message": "Invalid request data", |
1418 | + "extra": {"email": ["Enter a valid email address."]}, |
1419 | } |
1420 | - self.add_get_sso_discharge_response( |
1421 | - {'status': 401, 'json': {'error_list': [auth_error]}}) |
1422 | - self.assertEqual(1, login(self.make_args())) |
1423 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1424 | + self.add_get_sso_discharge_response( |
1425 | + {"status": 401, "json": {"error_list": [auth_error]}} |
1426 | + ) |
1427 | + self.assertEqual(1, login(**self.make_args())) |
1428 | self.assertEqual( |
1429 | - 'Enter your Ubuntu One SSO credentials.\n' |
1430 | - 'Login failed.\n' |
1431 | - 'Authentication error: Invalid request data\n' |
1432 | - 'email: Enter a valid email address.\n', |
1433 | - self.logger.output) |
1434 | + [ |
1435 | + "INFO:snapstore_client.cli:Enter your Ubuntu One SSO credentials.", |
1436 | + "ERROR:snapstore_client.cli:Login failed.", |
1437 | + "ERROR:snapstore_client.cli:Authentication error: Invalid request data", |
1438 | + "ERROR:snapstore_client.cli:email: Enter a valid email address.", |
1439 | + ], |
1440 | + logger.output, |
1441 | + ) |
1442 | |
1443 | @responses.activate |
1444 | def test_login_sso_unauthorized(self): |
1445 | - self.mock_input.return_value = 'user@example.org' |
1446 | - self.mock_getpass.return_value = 'secret' |
1447 | - self.add_issue_store_admin_response( |
1448 | - {'status': 200, 'json': {'macaroon': self.make_root_macaroon()}}) |
1449 | - auth_error = {'message': 'Provided email/password is not correct.'} |
1450 | - self.add_get_sso_discharge_response( |
1451 | - {'status': 401, 'json': {'error_list': [auth_error]}}) |
1452 | - self.assertEqual(1, login(self.make_args())) |
1453 | + self.mock_input.return_value = "user@example.org" |
1454 | + self.mock_getpass.return_value = "secret" |
1455 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1456 | + self.add_issue_store_admin_response( |
1457 | + {"status": 200, "json": {"macaroon": self.make_root_macaroon()}} |
1458 | + ) |
1459 | + auth_error = {"message": "Provided email/password is not correct."} |
1460 | + self.add_get_sso_discharge_response( |
1461 | + {"status": 401, "json": {"error_list": [auth_error]}} |
1462 | + ) |
1463 | + self.assertEqual(1, login(**self.make_args())) |
1464 | self.assertEqual( |
1465 | - 'Enter your Ubuntu One SSO credentials.\n' |
1466 | - 'Login failed.\n' |
1467 | - 'Authentication error: Provided email/password is not correct.\n', |
1468 | - self.logger.output) |
1469 | + [ |
1470 | + "INFO:snapstore_client.cli:Enter your Ubuntu One SSO credentials.", |
1471 | + "ERROR:snapstore_client.cli:Login failed.", |
1472 | + "ERROR:snapstore_client.cli:Authentication error: " |
1473 | + "Provided email/password is not correct.", |
1474 | + ], |
1475 | + logger.output, |
1476 | + ) |
1477 | |
1478 | @responses.activate |
1479 | def test_login_twofactor_required(self): |
1480 | - self.mock_input.side_effect = ('user@example.org', '123456') |
1481 | - self.mock_getpass.return_value = 'secret' |
1482 | - root = self.make_root_macaroon() |
1483 | - self.add_issue_store_admin_response( |
1484 | - {'status': 200, 'json': {'macaroon': root}}) |
1485 | - self.add_get_sso_discharge_response( |
1486 | - {'status': 401, |
1487 | - 'json': {'error_list': [{'code': 'twofactor-required'}]}}, |
1488 | - {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}) |
1489 | - self.assertEqual(0, login(self.make_args())) |
1490 | + self.mock_input.side_effect = ("user@example.org", "123456") |
1491 | + self.mock_getpass.return_value = "secret" |
1492 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1493 | + root = self.make_root_macaroon() |
1494 | + self.add_issue_store_admin_response( |
1495 | + {"status": 200, "json": {"macaroon": root}} |
1496 | + ) |
1497 | + self.add_get_sso_discharge_response( |
1498 | + { |
1499 | + "status": 401, |
1500 | + "json": {"error_list": [{"code": "twofactor-required"}]}, |
1501 | + }, |
1502 | + {"status": 200, "json": {"discharge_macaroon": "dummy"}}, |
1503 | + ) |
1504 | + self.assertEqual(0, login(**self.make_args())) |
1505 | |
1506 | self.assertIn( |
1507 | - 'Enter your Ubuntu One SSO credentials.', self.logger.output) |
1508 | - self.mock_input.assert_has_calls([ |
1509 | - mock.call('Email: '), mock.call('Second-factor auth: ')]) |
1510 | - self.mock_getpass.assert_called_once_with('Password: ') |
1511 | + "INFO:snapstore_client.cli:Enter your Ubuntu One SSO credentials.", |
1512 | + logger.output, |
1513 | + ) |
1514 | + self.mock_input.assert_has_calls( |
1515 | + [mock.call("Email: "), mock.call("Second-factor auth: ")] |
1516 | + ) |
1517 | + self.mock_getpass.assert_called_once_with("Password: ") |
1518 | self.assertEqual(3, len(responses.calls)) |
1519 | - self.assertEqual({ |
1520 | - 'email': 'user@example.org', |
1521 | - 'password': 'secret', |
1522 | - 'caveat_id': 'payload', |
1523 | - }, json.loads(responses.calls[1].request.body.decode())) |
1524 | - self.assertEqual({ |
1525 | - 'email': 'user@example.org', |
1526 | - 'password': 'secret', |
1527 | - 'caveat_id': 'payload', |
1528 | - 'otp': '123456', |
1529 | - }, json.loads(responses.calls[2].request.body.decode())) |
1530 | - self.assertThat(config.Config().parser, ContainsDict({ |
1531 | - 'store:default': MatchesDict({ |
1532 | - 'gw_url': Equals(self.default_gw_url), |
1533 | - 'sso_url': Equals(self.default_sso_url), |
1534 | - 'root': Equals(root), |
1535 | - 'unbound_discharge': Equals('dummy'), |
1536 | - 'email': Equals('user@example.org'), |
1537 | - }), |
1538 | - })) |
1539 | + self.assertEqual( |
1540 | + { |
1541 | + "email": "user@example.org", |
1542 | + "password": "secret", |
1543 | + "caveat_id": "payload", |
1544 | + }, |
1545 | + json.loads(responses.calls[1].request.body.decode()), |
1546 | + ) |
1547 | + self.assertEqual( |
1548 | + { |
1549 | + "email": "user@example.org", |
1550 | + "password": "secret", |
1551 | + "caveat_id": "payload", |
1552 | + "otp": "123456", |
1553 | + }, |
1554 | + json.loads(responses.calls[2].request.body.decode()), |
1555 | + ) |
1556 | + self.assertThat( |
1557 | + config.Config().parser, |
1558 | + ContainsDict( |
1559 | + { |
1560 | + "store:default": MatchesDict( |
1561 | + { |
1562 | + "gw_url": Equals(self.default_gw_url), |
1563 | + "sso_url": Equals(self.default_sso_url), |
1564 | + "root": Equals(root), |
1565 | + "unbound_discharge": Equals("dummy"), |
1566 | + "email": Equals("user@example.org"), |
1567 | + } |
1568 | + ), |
1569 | + } |
1570 | + ), |
1571 | + ) |
1572 | |
1573 | @responses.activate |
1574 | def test_login_twofactor_not_required(self): |
1575 | - self.mock_input.return_value = 'user@example.org' |
1576 | - self.mock_getpass.return_value = 'secret' |
1577 | - root = self.make_root_macaroon() |
1578 | - self.add_issue_store_admin_response( |
1579 | - {'status': 200, 'json': {'macaroon': root}}) |
1580 | - self.add_get_sso_discharge_response( |
1581 | - {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}) |
1582 | - login(self.make_args()) |
1583 | + self.mock_input.return_value = "user@example.org" |
1584 | + self.mock_getpass.return_value = "secret" |
1585 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1586 | + root = self.make_root_macaroon() |
1587 | + self.add_issue_store_admin_response( |
1588 | + {"status": 200, "json": {"macaroon": root}} |
1589 | + ) |
1590 | + self.add_get_sso_discharge_response( |
1591 | + {"status": 200, "json": {"discharge_macaroon": "dummy"}} |
1592 | + ) |
1593 | + login(**self.make_args()) |
1594 | |
1595 | self.assertIn( |
1596 | - 'Enter your Ubuntu One SSO credentials.', self.logger.output) |
1597 | - self.mock_input.assert_called_once_with('Email: ') |
1598 | - self.mock_getpass.assert_called_once_with('Password: ') |
1599 | + "INFO:snapstore_client.cli:Enter your Ubuntu One SSO credentials.", |
1600 | + logger.output, |
1601 | + ) |
1602 | + self.mock_input.assert_called_once_with("Email: ") |
1603 | + self.mock_getpass.assert_called_once_with("Password: ") |
1604 | self.assertEqual(2, len(responses.calls)) |
1605 | - self.assertEqual({ |
1606 | - 'email': 'user@example.org', |
1607 | - 'password': 'secret', |
1608 | - 'caveat_id': 'payload', |
1609 | - }, json.loads(responses.calls[1].request.body.decode())) |
1610 | - self.assertThat(config.Config().parser, ContainsDict({ |
1611 | - 'store:default': MatchesDict({ |
1612 | - 'gw_url': Equals(self.default_gw_url), |
1613 | - 'sso_url': Equals(self.default_sso_url), |
1614 | - 'root': Equals(root), |
1615 | - 'unbound_discharge': Equals('dummy'), |
1616 | - 'email': Equals('user@example.org'), |
1617 | - }), |
1618 | - })) |
1619 | + self.assertEqual( |
1620 | + { |
1621 | + "email": "user@example.org", |
1622 | + "password": "secret", |
1623 | + "caveat_id": "payload", |
1624 | + }, |
1625 | + json.loads(responses.calls[1].request.body.decode()), |
1626 | + ) |
1627 | + self.assertThat( |
1628 | + config.Config().parser, |
1629 | + ContainsDict( |
1630 | + { |
1631 | + "store:default": MatchesDict( |
1632 | + { |
1633 | + "gw_url": Equals(self.default_gw_url), |
1634 | + "sso_url": Equals(self.default_sso_url), |
1635 | + "root": Equals(root), |
1636 | + "unbound_discharge": Equals("dummy"), |
1637 | + "email": Equals("user@example.org"), |
1638 | + } |
1639 | + ), |
1640 | + } |
1641 | + ), |
1642 | + ) |
1643 | |
1644 | @responses.activate |
1645 | def test_login_with_email(self): |
1646 | self.mock_input.side_effect = Exception("shouldn't be called") |
1647 | - self.mock_getpass.return_value = 'secret' |
1648 | - root = self.make_root_macaroon() |
1649 | - self.add_issue_store_admin_response( |
1650 | - {'status': 200, 'json': {'macaroon': root}}) |
1651 | - self.add_get_sso_discharge_response( |
1652 | - {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}) |
1653 | - login(self.make_args(email='user@example.org')) |
1654 | + self.mock_getpass.return_value = "secret" |
1655 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1656 | + root = self.make_root_macaroon() |
1657 | + self.add_issue_store_admin_response( |
1658 | + {"status": 200, "json": {"macaroon": root}} |
1659 | + ) |
1660 | + self.add_get_sso_discharge_response( |
1661 | + {"status": 200, "json": {"discharge_macaroon": "dummy"}} |
1662 | + ) |
1663 | + login(**self.make_args(email="user@example.org")) |
1664 | |
1665 | self.assertIn( |
1666 | - 'Enter your Ubuntu One SSO credentials.', self.logger.output) |
1667 | + "INFO:snapstore_client.cli:Enter your Ubuntu One SSO credentials.", |
1668 | + logger.output, |
1669 | + ) |
1670 | self.mock_input.assert_not_called() |
1671 | - self.mock_getpass.assert_called_once_with('Password: ') |
1672 | + self.mock_getpass.assert_called_once_with("Password: ") |
1673 | self.assertEqual(2, len(responses.calls)) |
1674 | - self.assertEqual({ |
1675 | - 'email': 'user@example.org', |
1676 | - 'password': 'secret', |
1677 | - 'caveat_id': 'payload', |
1678 | - }, json.loads(responses.calls[1].request.body.decode())) |
1679 | - self.assertThat(config.Config().parser, ContainsDict({ |
1680 | - 'store:default': MatchesDict({ |
1681 | - 'gw_url': Equals(self.default_gw_url), |
1682 | - 'sso_url': Equals(self.default_sso_url), |
1683 | - 'root': Equals(root), |
1684 | - 'unbound_discharge': Equals('dummy'), |
1685 | - 'email': Equals('user@example.org'), |
1686 | - }), |
1687 | - })) |
1688 | + self.assertEqual( |
1689 | + { |
1690 | + "email": "user@example.org", |
1691 | + "password": "secret", |
1692 | + "caveat_id": "payload", |
1693 | + }, |
1694 | + json.loads(responses.calls[1].request.body.decode()), |
1695 | + ) |
1696 | + self.assertThat( |
1697 | + config.Config().parser, |
1698 | + ContainsDict( |
1699 | + { |
1700 | + "store:default": MatchesDict( |
1701 | + { |
1702 | + "gw_url": Equals(self.default_gw_url), |
1703 | + "sso_url": Equals(self.default_sso_url), |
1704 | + "root": Equals(root), |
1705 | + "unbound_discharge": Equals("dummy"), |
1706 | + "email": Equals("user@example.org"), |
1707 | + } |
1708 | + ), |
1709 | + } |
1710 | + ), |
1711 | + ) |
1712 | |
1713 | @responses.activate |
1714 | def test_store_url(self): |
1715 | - gw_url = 'http://otherstore.local:1234/' |
1716 | + gw_url = "http://otherstore.local:1234/" |
1717 | |
1718 | - self.mock_input.return_value = 'user@example.org' |
1719 | - self.mock_getpass.return_value = 'secret' |
1720 | + self.mock_input.return_value = "user@example.org" |
1721 | + self.mock_getpass.return_value = "secret" |
1722 | root = self.make_root_macaroon() |
1723 | self.add_issue_store_admin_response( |
1724 | - {'status': 200, 'json': {'macaroon': root}}, gw_url=gw_url) |
1725 | + {"status": 200, "json": {"macaroon": root}}, gw_url=gw_url |
1726 | + ) |
1727 | self.add_get_sso_discharge_response( |
1728 | - {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}) |
1729 | - login(self.make_args(store_url=gw_url)) |
1730 | + {"status": 200, "json": {"discharge_macaroon": "dummy"}} |
1731 | + ) |
1732 | + login(**self.make_args(store_url=gw_url)) |
1733 | |
1734 | self.assertEqual(2, len(responses.calls)) |
1735 | - self.assertEqual(responses.calls[0].request.url[:len(gw_url)], gw_url) |
1736 | - self.assertTrue( |
1737 | - responses.calls[1].request.url.startswith(self.default_sso_url)) |
1738 | - self.assertThat(config.Config().parser, ContainsDict({ |
1739 | - 'store:default': ContainsDict({ |
1740 | - 'gw_url': Equals(gw_url), |
1741 | - 'sso_url': Equals(self.default_sso_url), |
1742 | - }), |
1743 | - })) |
1744 | + self.assertEqual(responses.calls[0].request.url[: len(gw_url)], gw_url) |
1745 | + self.assertTrue(responses.calls[1].request.url.startswith(self.default_sso_url)) |
1746 | + self.assertThat( |
1747 | + config.Config().parser, |
1748 | + ContainsDict( |
1749 | + { |
1750 | + "store:default": ContainsDict( |
1751 | + { |
1752 | + "gw_url": Equals(gw_url), |
1753 | + "sso_url": Equals(self.default_sso_url), |
1754 | + } |
1755 | + ), |
1756 | + } |
1757 | + ), |
1758 | + ) |
1759 | |
1760 | @responses.activate |
1761 | def test_sso_url(self): |
1762 | - sso_url = 'https://othersso.local:1234/' |
1763 | + sso_url = "https://othersso.local:1234/" |
1764 | |
1765 | - self.mock_input.return_value = 'user@example.org' |
1766 | - self.mock_getpass.return_value = 'secret' |
1767 | + self.mock_input.return_value = "user@example.org" |
1768 | + self.mock_getpass.return_value = "secret" |
1769 | root = self.make_root_macaroon(sso_url=sso_url) |
1770 | - self.add_issue_store_admin_response( |
1771 | - {'status': 200, 'json': {'macaroon': root}}) |
1772 | + self.add_issue_store_admin_response({"status": 200, "json": {"macaroon": root}}) |
1773 | self.add_get_sso_discharge_response( |
1774 | - {'status': 200, 'json': {'discharge_macaroon': 'dummy'}}, |
1775 | - sso_url=sso_url) |
1776 | - login(self.make_args(sso_url=sso_url)) |
1777 | + {"status": 200, "json": {"discharge_macaroon": "dummy"}}, sso_url=sso_url |
1778 | + ) |
1779 | + login(**self.make_args(sso_url=sso_url)) |
1780 | |
1781 | self.assertEqual(2, len(responses.calls)) |
1782 | - self.assertTrue( |
1783 | - responses.calls[0].request.url.startswith(self.default_gw_url)) |
1784 | - self.assertEqual( |
1785 | - responses.calls[1].request.url[:len(sso_url)], sso_url) |
1786 | - self.assertThat(config.Config().parser, ContainsDict({ |
1787 | - 'store:default': ContainsDict({ |
1788 | - 'gw_url': Equals(self.default_gw_url), |
1789 | - 'sso_url': Equals(sso_url), |
1790 | - }), |
1791 | - })) |
1792 | + self.assertTrue(responses.calls[0].request.url.startswith(self.default_gw_url)) |
1793 | + self.assertEqual(responses.calls[1].request.url[: len(sso_url)], sso_url) |
1794 | + self.assertThat( |
1795 | + config.Config().parser, |
1796 | + ContainsDict( |
1797 | + { |
1798 | + "store:default": ContainsDict( |
1799 | + { |
1800 | + "gw_url": Equals(self.default_gw_url), |
1801 | + "sso_url": Equals(sso_url), |
1802 | + } |
1803 | + ), |
1804 | + } |
1805 | + ), |
1806 | + ) |
1807 | diff --git a/snapstore_client/logic/tests/test_overrides.py b/snapstore_client/logic/tests/test_overrides.py |
1808 | index 05e32fc..78c9132 100644 |
1809 | --- a/snapstore_client/logic/tests/test_overrides.py |
1810 | +++ b/snapstore_client/logic/tests/test_overrides.py |
1811 | @@ -20,295 +20,366 @@ from snapstore_client.tests import ( |
1812 | |
1813 | |
1814 | class OverridesTests(TestCase): |
1815 | - |
1816 | def test_list_overrides_no_store_config(self): |
1817 | self.useFixture(testfixtures.ConfigFixture(empty=True)) |
1818 | logger = self.useFixture(fixtures.FakeLogger()) |
1819 | - rc = list_overrides(factory.Args(snap_name='some-snap', series='16')) |
1820 | + rc = list_overrides(snap_name="some-snap", series="16") |
1821 | self.assertEqual(rc, 1) |
1822 | self.assertEqual( |
1823 | logger.output, |
1824 | - 'No store configuration found. ' |
1825 | - 'Have you run "snap-store-proxy-client login"?\n') |
1826 | + "No store configuration found. " |
1827 | + 'Have you run "snap-store-proxy-client login"?\n', |
1828 | + ) |
1829 | |
1830 | @responses.activate |
1831 | def test_list_overrides_online(self): |
1832 | self.useFixture(testfixtures.ConfigFixture()) |
1833 | - logger = self.useFixture(fixtures.FakeLogger()) |
1834 | - snap_id = factory.generate_snap_id() |
1835 | - overrides = [ |
1836 | - factory.SnapDeviceGateway.Override( |
1837 | - snap_id=snap_id, snap_name='mysnap'), |
1838 | - factory.SnapDeviceGateway.Override( |
1839 | - snap_id=snap_id, snap_name='mysnap', revision=3, |
1840 | - upstream_revision=4, channel='foo/stable', |
1841 | - architecture='i386'), |
1842 | - ] |
1843 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
1844 | - # they exist. |
1845 | - overrides_url = urljoin( |
1846 | - config.Config().store_section('default').get('gw_url'), |
1847 | - '/v2/metadata/overrides/mysnap') |
1848 | - responses.add( |
1849 | - 'GET', overrides_url, status=200, json={'overrides': overrides}) |
1850 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1851 | + snap_id = factory.generate_snap_id() |
1852 | + overrides = [ |
1853 | + factory.SnapDeviceGateway.Override(snap_id=snap_id, snap_name="mysnap"), |
1854 | + factory.SnapDeviceGateway.Override( |
1855 | + snap_id=snap_id, |
1856 | + snap_name="mysnap", |
1857 | + revision=3, |
1858 | + upstream_revision=4, |
1859 | + channel="foo/stable", |
1860 | + architecture="i386", |
1861 | + ), |
1862 | + ] |
1863 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
1864 | + # they exist. |
1865 | + overrides_url = urljoin( |
1866 | + config.Config().store_section("default").get("gw_url"), |
1867 | + "/v2/metadata/overrides/mysnap", |
1868 | + ) |
1869 | + responses.add( |
1870 | + "GET", overrides_url, status=200, json={"overrides": overrides} |
1871 | + ) |
1872 | |
1873 | - list_overrides( |
1874 | - factory.Args(snap_name='mysnap', series='16', password=False)) |
1875 | + list_overrides(snap_name="mysnap", series="16", password=False) |
1876 | self.assertEqual( |
1877 | - 'mysnap stable amd64 1 (upstream 2)\n' |
1878 | - 'mysnap foo/stable i386 3 (upstream 4)\n', |
1879 | - logger.output) |
1880 | + [ |
1881 | + "INFO:snapstore_client.cli:mysnap stable amd64 1 (upstream 2)", |
1882 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 3 (upstream 4)", |
1883 | + ], |
1884 | + logger.output, |
1885 | + ) |
1886 | # We shouldn't have Basic Authorization headers, but Macaroon |
1887 | - self.assertNotIn( |
1888 | - 'Basic', |
1889 | - responses.calls[0].request.headers['Authorization']) |
1890 | + self.assertNotIn("Basic", responses.calls[0].request.headers["Authorization"]) |
1891 | |
1892 | @responses.activate |
1893 | def test_list_overrides_offline(self): |
1894 | self.useFixture(testfixtures.ConfigFixture()) |
1895 | - logger = self.useFixture(fixtures.FakeLogger()) |
1896 | - snap_id = factory.generate_snap_id() |
1897 | - overrides = [ |
1898 | - factory.SnapDeviceGateway.Override( |
1899 | - snap_id=snap_id, snap_name='mysnap'), |
1900 | - factory.SnapDeviceGateway.Override( |
1901 | - snap_id=snap_id, snap_name='mysnap', revision=3, |
1902 | - upstream_revision=4, channel='foo/stable', |
1903 | - architecture='i386'), |
1904 | - ] |
1905 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
1906 | - # they exist. |
1907 | - overrides_url = urljoin( |
1908 | - config.Config().store_section('default').get('gw_url'), |
1909 | - '/v2/metadata/overrides/mysnap') |
1910 | - responses.add( |
1911 | - 'GET', overrides_url, status=200, json={'overrides': overrides}) |
1912 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1913 | + snap_id = factory.generate_snap_id() |
1914 | + overrides = [ |
1915 | + factory.SnapDeviceGateway.Override(snap_id=snap_id, snap_name="mysnap"), |
1916 | + factory.SnapDeviceGateway.Override( |
1917 | + snap_id=snap_id, |
1918 | + snap_name="mysnap", |
1919 | + revision=3, |
1920 | + upstream_revision=4, |
1921 | + channel="foo/stable", |
1922 | + architecture="i386", |
1923 | + ), |
1924 | + ] |
1925 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
1926 | + # they exist. |
1927 | + overrides_url = urljoin( |
1928 | + config.Config().store_section("default").get("gw_url"), |
1929 | + "/v2/metadata/overrides/mysnap", |
1930 | + ) |
1931 | + responses.add( |
1932 | + "GET", overrides_url, status=200, json={"overrides": overrides} |
1933 | + ) |
1934 | |
1935 | - list_overrides( |
1936 | - factory.Args(snap_name='mysnap', series='16', password='test')) |
1937 | + list_overrides(snap_name="mysnap", series="16", password="test") |
1938 | self.assertEqual( |
1939 | - 'mysnap stable amd64 1 (upstream 2)\n' |
1940 | - 'mysnap foo/stable i386 3 (upstream 4)\n', |
1941 | - logger.output) |
1942 | + [ |
1943 | + "INFO:snapstore_client.cli:mysnap stable amd64 1 (upstream 2)", |
1944 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 3 (upstream 4)", |
1945 | + ], |
1946 | + logger.output, |
1947 | + ) |
1948 | self.assertEqual( |
1949 | - 'Basic YWRtaW46dGVzdA==', |
1950 | - responses.calls[0].request.headers['Authorization']) |
1951 | + "Basic YWRtaW46dGVzdA==", |
1952 | + responses.calls[0].request.headers["Authorization"], |
1953 | + ) |
1954 | |
1955 | def test_override_no_store_config(self): |
1956 | self.useFixture(testfixtures.ConfigFixture(empty=True)) |
1957 | logger = self.useFixture(fixtures.FakeLogger()) |
1958 | - rc = override(factory.Args( |
1959 | - snap_name='some-snap', channel_map_entries=['stable=1'], |
1960 | - series='16', |
1961 | - password=False)) |
1962 | + rc = override( |
1963 | + snap_name="some-snap", |
1964 | + channel_map_entries=["stable=1"], |
1965 | + series="16", |
1966 | + password=False, |
1967 | + ) |
1968 | self.assertEqual(rc, 1) |
1969 | self.assertEqual( |
1970 | logger.output, |
1971 | - 'No store configuration found. ' |
1972 | - 'Have you run "snap-store-proxy-client login"?\n') |
1973 | + "No store configuration found. " |
1974 | + 'Have you run "snap-store-proxy-client login"?\n', |
1975 | + ) |
1976 | |
1977 | @responses.activate |
1978 | def test_override_online(self): |
1979 | self.useFixture(testfixtures.ConfigFixture()) |
1980 | - logger = self.useFixture(fixtures.FakeLogger()) |
1981 | - snap_id = factory.generate_snap_id() |
1982 | - overrides = [ |
1983 | - factory.SnapDeviceGateway.Override( |
1984 | - snap_id=snap_id, snap_name='mysnap'), |
1985 | - factory.SnapDeviceGateway.Override( |
1986 | - snap_id=snap_id, snap_name='mysnap', revision=3, |
1987 | - upstream_revision=4, channel='foo/stable', |
1988 | - architecture='i386'), |
1989 | - ] |
1990 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
1991 | - # they exist. |
1992 | - overrides_url = urljoin( |
1993 | - config.Config().store_section('default').get('gw_url'), |
1994 | - '/v2/metadata/overrides') |
1995 | - responses.add( |
1996 | - 'POST', overrides_url, status=200, json={'overrides': overrides}) |
1997 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
1998 | + snap_id = factory.generate_snap_id() |
1999 | + overrides = [ |
2000 | + factory.SnapDeviceGateway.Override(snap_id=snap_id, snap_name="mysnap"), |
2001 | + factory.SnapDeviceGateway.Override( |
2002 | + snap_id=snap_id, |
2003 | + snap_name="mysnap", |
2004 | + revision=3, |
2005 | + upstream_revision=4, |
2006 | + channel="foo/stable", |
2007 | + architecture="i386", |
2008 | + ), |
2009 | + ] |
2010 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2011 | + # they exist. |
2012 | + overrides_url = urljoin( |
2013 | + config.Config().store_section("default").get("gw_url"), |
2014 | + "/v2/metadata/overrides", |
2015 | + ) |
2016 | + responses.add( |
2017 | + "POST", overrides_url, status=200, json={"overrides": overrides} |
2018 | + ) |
2019 | |
2020 | - override(factory.Args( |
2021 | - snap_name='mysnap', |
2022 | - channel_map_entries=['stable=1', 'foo/stable=3'], |
2023 | - series='16', |
2024 | - password=False)) |
2025 | - self.assertEqual([ |
2026 | - { |
2027 | - 'snap_name': 'mysnap', |
2028 | - 'revision': 1, |
2029 | - 'channel': 'stable', |
2030 | - 'series': '16', |
2031 | - }, |
2032 | - { |
2033 | - 'snap_name': 'mysnap', |
2034 | - 'revision': 3, |
2035 | - 'channel': 'foo/stable', |
2036 | - 'series': '16', |
2037 | - }, |
2038 | - ], json.loads(responses.calls[0].request.body.decode())) |
2039 | + override( |
2040 | + snap_name="mysnap", |
2041 | + channel_map_entries=["stable=1", "foo/stable=3"], |
2042 | + series="16", |
2043 | + password=False, |
2044 | + ) |
2045 | + self.assertEqual( |
2046 | + [ |
2047 | + { |
2048 | + "snap_name": "mysnap", |
2049 | + "revision": 1, |
2050 | + "channel": "stable", |
2051 | + "series": "16", |
2052 | + }, |
2053 | + { |
2054 | + "snap_name": "mysnap", |
2055 | + "revision": 3, |
2056 | + "channel": "foo/stable", |
2057 | + "series": "16", |
2058 | + }, |
2059 | + ], |
2060 | + json.loads(responses.calls[0].request.body.decode()), |
2061 | + ) |
2062 | self.assertEqual( |
2063 | - 'mysnap stable amd64 1 (upstream 2)\n' |
2064 | - 'mysnap foo/stable i386 3 (upstream 4)\n', |
2065 | - logger.output) |
2066 | + [ |
2067 | + "INFO:snapstore_client.cli:mysnap stable amd64 1 (upstream 2)", |
2068 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 3 (upstream 4)", |
2069 | + ], |
2070 | + logger.output, |
2071 | + ) |
2072 | # We shouldn't have Basic Authorization headers, but Macaroon |
2073 | - self.assertNotIn( |
2074 | - 'Basic', |
2075 | - responses.calls[0].request.headers['Authorization']) |
2076 | + self.assertNotIn("Basic", responses.calls[0].request.headers["Authorization"]) |
2077 | |
2078 | @responses.activate |
2079 | def test_override_offline(self): |
2080 | self.useFixture(testfixtures.ConfigFixture()) |
2081 | - logger = self.useFixture(fixtures.FakeLogger()) |
2082 | - snap_id = factory.generate_snap_id() |
2083 | - overrides = [ |
2084 | - factory.SnapDeviceGateway.Override( |
2085 | - snap_id=snap_id, snap_name='mysnap'), |
2086 | - factory.SnapDeviceGateway.Override( |
2087 | - snap_id=snap_id, snap_name='mysnap', revision=3, |
2088 | - upstream_revision=4, channel='foo/stable', |
2089 | - architecture='i386'), |
2090 | - ] |
2091 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2092 | - # they exist. |
2093 | - overrides_url = urljoin( |
2094 | - config.Config().store_section('default').get('gw_url'), |
2095 | - '/v2/metadata/overrides') |
2096 | - responses.add( |
2097 | - 'POST', overrides_url, status=200, json={'overrides': overrides}) |
2098 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
2099 | + snap_id = factory.generate_snap_id() |
2100 | + overrides = [ |
2101 | + factory.SnapDeviceGateway.Override(snap_id=snap_id, snap_name="mysnap"), |
2102 | + factory.SnapDeviceGateway.Override( |
2103 | + snap_id=snap_id, |
2104 | + snap_name="mysnap", |
2105 | + revision=3, |
2106 | + upstream_revision=4, |
2107 | + channel="foo/stable", |
2108 | + architecture="i386", |
2109 | + ), |
2110 | + ] |
2111 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2112 | + # they exist. |
2113 | + overrides_url = urljoin( |
2114 | + config.Config().store_section("default").get("gw_url"), |
2115 | + "/v2/metadata/overrides", |
2116 | + ) |
2117 | + responses.add( |
2118 | + "POST", overrides_url, status=200, json={"overrides": overrides} |
2119 | + ) |
2120 | |
2121 | - override(factory.Args( |
2122 | - snap_name='mysnap', |
2123 | - channel_map_entries=['stable=1', 'foo/stable=3'], |
2124 | - series='16', |
2125 | - password='test')) |
2126 | - self.assertEqual([ |
2127 | - { |
2128 | - 'snap_name': 'mysnap', |
2129 | - 'revision': 1, |
2130 | - 'channel': 'stable', |
2131 | - 'series': '16', |
2132 | - }, |
2133 | - { |
2134 | - 'snap_name': 'mysnap', |
2135 | - 'revision': 3, |
2136 | - 'channel': 'foo/stable', |
2137 | - 'series': '16', |
2138 | - }, |
2139 | - ], json.loads(responses.calls[0].request.body.decode())) |
2140 | + override( |
2141 | + snap_name="mysnap", |
2142 | + channel_map_entries=["stable=1", "foo/stable=3"], |
2143 | + series="16", |
2144 | + password="test", |
2145 | + ) |
2146 | + self.assertEqual( |
2147 | + [ |
2148 | + { |
2149 | + "snap_name": "mysnap", |
2150 | + "revision": 1, |
2151 | + "channel": "stable", |
2152 | + "series": "16", |
2153 | + }, |
2154 | + { |
2155 | + "snap_name": "mysnap", |
2156 | + "revision": 3, |
2157 | + "channel": "foo/stable", |
2158 | + "series": "16", |
2159 | + }, |
2160 | + ], |
2161 | + json.loads(responses.calls[0].request.body.decode()), |
2162 | + ) |
2163 | self.assertEqual( |
2164 | - 'mysnap stable amd64 1 (upstream 2)\n' |
2165 | - 'mysnap foo/stable i386 3 (upstream 4)\n', |
2166 | - logger.output) |
2167 | + [ |
2168 | + "INFO:snapstore_client.cli:mysnap stable amd64 1 (upstream 2)", |
2169 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 3 (upstream 4)", |
2170 | + ], |
2171 | + logger.output, |
2172 | + ) |
2173 | self.assertEqual( |
2174 | - 'Basic YWRtaW46dGVzdA==', |
2175 | - responses.calls[0].request.headers['Authorization']) |
2176 | + "Basic YWRtaW46dGVzdA==", |
2177 | + responses.calls[0].request.headers["Authorization"], |
2178 | + ) |
2179 | |
2180 | def test_delete_override_no_store_config(self): |
2181 | self.useFixture(testfixtures.ConfigFixture(empty=True)) |
2182 | logger = self.useFixture(fixtures.FakeLogger()) |
2183 | - rc = delete_override(factory.Args( |
2184 | - snap_name='some-snap', channels=['stable'], |
2185 | - series='16', password=False)) |
2186 | + rc = delete_override( |
2187 | + snap_name="some-snap", channels=["stable"], series="16", password=False |
2188 | + ) |
2189 | self.assertEqual(rc, 1) |
2190 | self.assertEqual( |
2191 | logger.output, |
2192 | - 'No store configuration found. ' |
2193 | - 'Have you run "snap-store-proxy-client login"?\n') |
2194 | + "No store configuration found. " |
2195 | + 'Have you run "snap-store-proxy-client login"?\n', |
2196 | + ) |
2197 | |
2198 | @responses.activate |
2199 | def test_delete_override_online(self): |
2200 | self.useFixture(testfixtures.ConfigFixture()) |
2201 | - logger = self.useFixture(fixtures.FakeLogger()) |
2202 | - snap_id = factory.generate_snap_id() |
2203 | - overrides = [ |
2204 | - factory.SnapDeviceGateway.Override( |
2205 | - snap_id=snap_id, snap_name='mysnap', revision=None), |
2206 | - factory.SnapDeviceGateway.Override( |
2207 | - snap_id=snap_id, snap_name='mysnap', revision=None, |
2208 | - upstream_revision=4, channel='foo/stable', |
2209 | - architecture='i386'), |
2210 | - ] |
2211 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2212 | - # they exist. |
2213 | - overrides_url = urljoin( |
2214 | - config.Config().store_section('default').get('gw_url'), |
2215 | - '/v2/metadata/overrides') |
2216 | - responses.add( |
2217 | - 'POST', overrides_url, status=200, json={'overrides': overrides}) |
2218 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
2219 | + snap_id = factory.generate_snap_id() |
2220 | + overrides = [ |
2221 | + factory.SnapDeviceGateway.Override( |
2222 | + snap_id=snap_id, snap_name="mysnap", revision=None |
2223 | + ), |
2224 | + factory.SnapDeviceGateway.Override( |
2225 | + snap_id=snap_id, |
2226 | + snap_name="mysnap", |
2227 | + revision=None, |
2228 | + upstream_revision=4, |
2229 | + channel="foo/stable", |
2230 | + architecture="i386", |
2231 | + ), |
2232 | + ] |
2233 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2234 | + # they exist. |
2235 | + overrides_url = urljoin( |
2236 | + config.Config().store_section("default").get("gw_url"), |
2237 | + "/v2/metadata/overrides", |
2238 | + ) |
2239 | + responses.add( |
2240 | + "POST", overrides_url, status=200, json={"overrides": overrides} |
2241 | + ) |
2242 | |
2243 | - delete_override(factory.Args( |
2244 | - snap_name='mysnap', |
2245 | - channels=['stable', 'foo/stable'], |
2246 | - series='16', |
2247 | - password=False)) |
2248 | - self.assertEqual([ |
2249 | - { |
2250 | - 'snap_name': 'mysnap', |
2251 | - 'revision': None, |
2252 | - 'channel': 'stable', |
2253 | - 'series': '16', |
2254 | - }, |
2255 | - { |
2256 | - 'snap_name': 'mysnap', |
2257 | - 'revision': None, |
2258 | - 'channel': 'foo/stable', |
2259 | - 'series': '16', |
2260 | - }, |
2261 | - ], json.loads(responses.calls[0].request.body.decode())) |
2262 | + delete_override( |
2263 | + snap_name="mysnap", |
2264 | + channels=["stable", "foo/stable"], |
2265 | + series="16", |
2266 | + password=False, |
2267 | + ) |
2268 | self.assertEqual( |
2269 | - 'mysnap stable amd64 is tracking upstream (revision 2)\n' |
2270 | - 'mysnap foo/stable i386 is tracking upstream (revision 4)\n', |
2271 | - logger.output) |
2272 | + [ |
2273 | + { |
2274 | + "snap_name": "mysnap", |
2275 | + "revision": None, |
2276 | + "channel": "stable", |
2277 | + "series": "16", |
2278 | + }, |
2279 | + { |
2280 | + "snap_name": "mysnap", |
2281 | + "revision": None, |
2282 | + "channel": "foo/stable", |
2283 | + "series": "16", |
2284 | + }, |
2285 | + ], |
2286 | + json.loads(responses.calls[0].request.body.decode()), |
2287 | + ) |
2288 | + self.assertEqual( |
2289 | + [ |
2290 | + "INFO:snapstore_client.cli:mysnap stable amd64 is tracking upstream " |
2291 | + "(revision 2)", |
2292 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 is tracking upstream " |
2293 | + "(revision 4)", |
2294 | + ], |
2295 | + logger.output, |
2296 | + ) |
2297 | # We shouldn't have Basic Authorization headers, but Macaroon |
2298 | - self.assertNotIn( |
2299 | - 'Basic', |
2300 | - responses.calls[0].request.headers['Authorization']) |
2301 | + self.assertNotIn("Basic", responses.calls[0].request.headers["Authorization"]) |
2302 | |
2303 | @responses.activate |
2304 | def test_delete_override_offline(self): |
2305 | self.useFixture(testfixtures.ConfigFixture()) |
2306 | - logger = self.useFixture(fixtures.FakeLogger()) |
2307 | - snap_id = factory.generate_snap_id() |
2308 | - overrides = [ |
2309 | - factory.SnapDeviceGateway.Override( |
2310 | - snap_id=snap_id, snap_name='mysnap', revision=None), |
2311 | - factory.SnapDeviceGateway.Override( |
2312 | - snap_id=snap_id, snap_name='mysnap', revision=None, |
2313 | - upstream_revision=4, channel='foo/stable', |
2314 | - architecture='i386'), |
2315 | - ] |
2316 | - # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2317 | - # they exist. |
2318 | - overrides_url = urljoin( |
2319 | - config.Config().store_section('default').get('gw_url'), |
2320 | - '/v2/metadata/overrides') |
2321 | - responses.add( |
2322 | - 'POST', overrides_url, status=200, json={'overrides': overrides}) |
2323 | + with self.assertLogs("snapstore_client.cli", "DEBUG") as logger: |
2324 | + snap_id = factory.generate_snap_id() |
2325 | + overrides = [ |
2326 | + factory.SnapDeviceGateway.Override( |
2327 | + snap_id=snap_id, snap_name="mysnap", revision=None |
2328 | + ), |
2329 | + factory.SnapDeviceGateway.Override( |
2330 | + snap_id=snap_id, |
2331 | + snap_name="mysnap", |
2332 | + revision=None, |
2333 | + upstream_revision=4, |
2334 | + channel="foo/stable", |
2335 | + architecture="i386", |
2336 | + ), |
2337 | + ] |
2338 | + # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once |
2339 | + # they exist. |
2340 | + overrides_url = urljoin( |
2341 | + config.Config().store_section("default").get("gw_url"), |
2342 | + "/v2/metadata/overrides", |
2343 | + ) |
2344 | + responses.add( |
2345 | + "POST", overrides_url, status=200, json={"overrides": overrides} |
2346 | + ) |
2347 | |
2348 | - delete_override(factory.Args( |
2349 | - snap_name='mysnap', |
2350 | - channels=['stable', 'foo/stable'], |
2351 | - series='16', |
2352 | - password='test')) |
2353 | - self.assertEqual([ |
2354 | - { |
2355 | - 'snap_name': 'mysnap', |
2356 | - 'revision': None, |
2357 | - 'channel': 'stable', |
2358 | - 'series': '16', |
2359 | - }, |
2360 | - { |
2361 | - 'snap_name': 'mysnap', |
2362 | - 'revision': None, |
2363 | - 'channel': 'foo/stable', |
2364 | - 'series': '16', |
2365 | - }, |
2366 | - ], json.loads(responses.calls[0].request.body.decode())) |
2367 | + delete_override( |
2368 | + snap_name="mysnap", |
2369 | + channels=["stable", "foo/stable"], |
2370 | + series="16", |
2371 | + password="test", |
2372 | + ) |
2373 | + self.assertEqual( |
2374 | + [ |
2375 | + { |
2376 | + "snap_name": "mysnap", |
2377 | + "revision": None, |
2378 | + "channel": "stable", |
2379 | + "series": "16", |
2380 | + }, |
2381 | + { |
2382 | + "snap_name": "mysnap", |
2383 | + "revision": None, |
2384 | + "channel": "foo/stable", |
2385 | + "series": "16", |
2386 | + }, |
2387 | + ], |
2388 | + json.loads(responses.calls[0].request.body.decode()), |
2389 | + ) |
2390 | self.assertEqual( |
2391 | - 'mysnap stable amd64 is tracking upstream (revision 2)\n' |
2392 | - 'mysnap foo/stable i386 is tracking upstream (revision 4)\n', |
2393 | - logger.output) |
2394 | + [ |
2395 | + "INFO:snapstore_client.cli:mysnap stable amd64 is tracking upstream " |
2396 | + "(revision 2)", |
2397 | + "INFO:snapstore_client.cli:mysnap foo/stable i386 is tracking upstream " |
2398 | + "(revision 4)", |
2399 | + ], |
2400 | + logger.output, |
2401 | + ) |
2402 | self.assertEqual( |
2403 | - 'Basic YWRtaW46dGVzdA==', |
2404 | - responses.calls[0].request.headers['Authorization']) |
2405 | + "Basic YWRtaW46dGVzdA==", |
2406 | + responses.calls[0].request.headers["Authorization"], |
2407 | + ) |
2408 | diff --git a/snapstore_client/logic/tests/test_push.py b/snapstore_client/logic/tests/test_push.py |
2409 | index cda240c..e96088f 100644 |
2410 | --- a/snapstore_client/logic/tests/test_push.py |
2411 | +++ b/snapstore_client/logic/tests/test_push.py |
2412 | @@ -18,187 +18,201 @@ from snapstore_client.logic.push import ( |
2413 | |
2414 | |
2415 | class pushTests(TestCase): |
2416 | - |
2417 | def setUp(self): |
2418 | super().setUp() |
2419 | - self.default_gw_url = 'http://store.local' |
2420 | + self.default_gw_url = "http://store.local" |
2421 | self.logger = self.useFixture(fixtures.FakeLogger()) |
2422 | current_path = Path(__file__).resolve().parent |
2423 | - with (current_path / 'test-snap-map.json').open() as fh: |
2424 | + with (current_path / "test-snap-map.json").open() as fh: |
2425 | self.snap_map = json.load(fh) |
2426 | - with (current_path / 'test-snap-assert.assert').open() as fh: |
2427 | + with (current_path / "test-snap-assert.assert").open() as fh: |
2428 | self.snap_assert = fh.read().splitlines() |
2429 | - self.store = {'gw_url': self.default_gw_url} |
2430 | + self.store = {"gw_url": self.default_gw_url} |
2431 | |
2432 | @responses.activate |
2433 | def test_push_ident(self): |
2434 | - ident_url = urljoin(self.default_gw_url, '/snaps/update') |
2435 | - responses.add('POST', ident_url, status=200) |
2436 | + ident_url = urljoin(self.default_gw_url, "/snaps/update") |
2437 | + responses.add("POST", ident_url, status=200) |
2438 | |
2439 | - _push_ident(self.store, 'test', self.snap_map) |
2440 | + _push_ident(self.store, "test", self.snap_map) |
2441 | |
2442 | request = responses.calls[0].request |
2443 | - payload = json.loads(request.body.decode('utf-8')) |
2444 | + payload = json.loads(request.body.decode("utf-8")) |
2445 | |
2446 | self.assertEqual( |
2447 | - payload['snaps'][0]['package_type'], 'snap', |
2448 | + payload["snaps"][0]["package_type"], |
2449 | + "snap", |
2450 | ) |
2451 | self.assertEqual( |
2452 | - 'Basic YWRtaW46dGVzdA==', request.headers['Authorization'], |
2453 | + "Basic YWRtaW46dGVzdA==", |
2454 | + request.headers["Authorization"], |
2455 | ) |
2456 | |
2457 | @responses.activate |
2458 | def test_push_ident_failed(self): |
2459 | - ident_url = urljoin(self.default_gw_url, '/snaps/update') |
2460 | - responses.add('POST', ident_url, status=500) |
2461 | + ident_url = urljoin(self.default_gw_url, "/snaps/update") |
2462 | + responses.add("POST", ident_url, status=500) |
2463 | |
2464 | - self.assertRaises( |
2465 | - pushException, _push_ident, self.store, 'test', self.snap_map) |
2466 | + self.assertRaises(pushException, _push_ident, self.store, "test", self.snap_map) |
2467 | |
2468 | @responses.activate |
2469 | def test_push_revs(self): |
2470 | - revs_url = urljoin(self.default_gw_url, '/revisions/create') |
2471 | - responses.add('POST', revs_url, status=201) |
2472 | + revs_url = urljoin(self.default_gw_url, "/revisions/create") |
2473 | + responses.add("POST", revs_url, status=201) |
2474 | |
2475 | - _push_revs(self.store, 'test', self.snap_map) |
2476 | + _push_revs(self.store, "test", self.snap_map) |
2477 | |
2478 | request = responses.calls[0].request |
2479 | - payload = json.loads(request.body.decode('utf-8')) |
2480 | + payload = json.loads(request.body.decode("utf-8")) |
2481 | |
2482 | - self.assertEqual(payload[0]['package_type'], 'snap') |
2483 | + self.assertEqual(payload[0]["package_type"], "snap") |
2484 | self.assertEqual( |
2485 | - 'Basic YWRtaW46dGVzdA==', request.headers['Authorization'], |
2486 | + "Basic YWRtaW46dGVzdA==", |
2487 | + request.headers["Authorization"], |
2488 | ) |
2489 | |
2490 | @responses.activate |
2491 | def test_push_revs_unexpected_status_code(self): |
2492 | - revs_url = urljoin(self.default_gw_url, '/revisions/create') |
2493 | - responses.add('POST', revs_url, status=302) |
2494 | + revs_url = urljoin(self.default_gw_url, "/revisions/create") |
2495 | + responses.add("POST", revs_url, status=302) |
2496 | |
2497 | - self.assertRaises( |
2498 | - pushException, _push_revs, self.store, 'test', self.snap_map) |
2499 | + self.assertRaises(pushException, _push_revs, self.store, "test", self.snap_map) |
2500 | |
2501 | @responses.activate |
2502 | def test_push_revs_failed(self): |
2503 | - revs_url = urljoin(self.default_gw_url, '/revisions/create') |
2504 | - responses.add('POST', revs_url, status=500) |
2505 | + revs_url = urljoin(self.default_gw_url, "/revisions/create") |
2506 | + responses.add("POST", revs_url, status=500) |
2507 | |
2508 | - self.assertRaises( |
2509 | - pushException, _push_revs, self.store, 'test', self.snap_map) |
2510 | + self.assertRaises(pushException, _push_revs, self.store, "test", self.snap_map) |
2511 | |
2512 | @responses.activate |
2513 | def test_push_map(self): |
2514 | - filter_url = urljoin(self.default_gw_url, '/channelmaps/filter') |
2515 | - update_url = urljoin(self.default_gw_url, '/channelmaps/update') |
2516 | - responses.add('POST', filter_url, status=200, json={}) |
2517 | - responses.add('POST', update_url, status=200) |
2518 | + filter_url = urljoin(self.default_gw_url, "/channelmaps/filter") |
2519 | + update_url = urljoin(self.default_gw_url, "/channelmaps/update") |
2520 | + responses.add("POST", filter_url, status=200, json={}) |
2521 | + responses.add("POST", update_url, status=200) |
2522 | |
2523 | - _push_channelmap(self.store, 'test', self.snap_map) |
2524 | + _push_channelmap(self.store, "test", self.snap_map) |
2525 | |
2526 | filter_request = responses.calls[0].request |
2527 | - filter_payload = json.loads(filter_request.body.decode('utf-8')) |
2528 | + filter_payload = json.loads(filter_request.body.decode("utf-8")) |
2529 | self.assertEqual( |
2530 | - filter_payload['filters'], |
2531 | + filter_payload["filters"], |
2532 | [ |
2533 | { |
2534 | - 'series': '16', |
2535 | - 'package_type': 'snap', |
2536 | - 'snap_id': self.snap_map['snap-id'], |
2537 | + "series": "16", |
2538 | + "package_type": "snap", |
2539 | + "snap_id": self.snap_map["snap-id"], |
2540 | }, |
2541 | ], |
2542 | ) |
2543 | self.assertEqual( |
2544 | - 'Basic YWRtaW46dGVzdA==', filter_request.headers['Authorization'], |
2545 | + "Basic YWRtaW46dGVzdA==", |
2546 | + filter_request.headers["Authorization"], |
2547 | ) |
2548 | |
2549 | chanmap_update_request = responses.calls[1].request |
2550 | chanmap_update_payload = json.loads( |
2551 | - chanmap_update_request.body.decode('utf-8'), |
2552 | + chanmap_update_request.body.decode("utf-8"), |
2553 | ) |
2554 | self.assertEqual( |
2555 | - chanmap_update_payload['release_requests'][0]['package_type'], |
2556 | - 'snap', |
2557 | + chanmap_update_payload["release_requests"][0]["package_type"], |
2558 | + "snap", |
2559 | ) |
2560 | |
2561 | @responses.activate |
2562 | def test_push_map_failed(self): |
2563 | - filter_url = urljoin(self.default_gw_url, '/channelmaps/filter') |
2564 | - update_url = urljoin(self.default_gw_url, '/channelmaps/update') |
2565 | - responses.add('POST', filter_url, status=200, json={}) |
2566 | - responses.add('POST', update_url, status=500) |
2567 | + filter_url = urljoin(self.default_gw_url, "/channelmaps/filter") |
2568 | + update_url = urljoin(self.default_gw_url, "/channelmaps/update") |
2569 | + responses.add("POST", filter_url, status=200, json={}) |
2570 | + responses.add("POST", update_url, status=500) |
2571 | |
2572 | self.assertRaises( |
2573 | - pushException, _push_channelmap, |
2574 | - self.store, 'test', self.snap_map) |
2575 | + pushException, _push_channelmap, self.store, "test", self.snap_map |
2576 | + ) |
2577 | |
2578 | @responses.activate |
2579 | def test_push_map_exists(self): |
2580 | - filter_url = urljoin(self.default_gw_url, '/channelmaps/filter') |
2581 | - update_url = urljoin(self.default_gw_url, '/channelmaps/update') |
2582 | - exists = {'channelmaps': ['thing that exists']} |
2583 | - responses.add('POST', filter_url, status=200, json=exists) |
2584 | - responses.add('POST', update_url, status=200) |
2585 | + filter_url = urljoin(self.default_gw_url, "/channelmaps/filter") |
2586 | + update_url = urljoin(self.default_gw_url, "/channelmaps/update") |
2587 | + exists = {"channelmaps": ["thing that exists"]} |
2588 | + responses.add("POST", filter_url, status=200, json=exists) |
2589 | + responses.add("POST", update_url, status=200) |
2590 | |
2591 | self.assertRaises( |
2592 | - ChannelMapExistsException, _push_channelmap, |
2593 | - self.store, 'test', self.snap_map) |
2594 | + ChannelMapExistsException, |
2595 | + _push_channelmap, |
2596 | + self.store, |
2597 | + "test", |
2598 | + self.snap_map, |
2599 | + ) |
2600 | |
2601 | @responses.activate |
2602 | def test_push_map_exists_force_push(self): |
2603 | - filter_url = urljoin(self.default_gw_url, '/channelmaps/filter') |
2604 | - update_url = urljoin(self.default_gw_url, '/channelmaps/update') |
2605 | - exists = {'channelmaps': ['thing that exists']} |
2606 | - responses.add('POST', filter_url, status=200, json=exists) |
2607 | - responses.add('POST', update_url, status=200) |
2608 | + filter_url = urljoin(self.default_gw_url, "/channelmaps/filter") |
2609 | + update_url = urljoin(self.default_gw_url, "/channelmaps/update") |
2610 | + exists = {"channelmaps": ["thing that exists"]} |
2611 | + responses.add("POST", filter_url, status=200, json=exists) |
2612 | + responses.add("POST", update_url, status=200) |
2613 | |
2614 | - _push_channelmap(self.store, 'test', self.snap_map, True) |
2615 | + _push_channelmap(self.store, "test", self.snap_map, True) |
2616 | |
2617 | self.assertEqual( |
2618 | - 'Basic YWRtaW46dGVzdA==', |
2619 | - responses.calls[0].request.headers['Authorization']) |
2620 | + "Basic YWRtaW46dGVzdA==", |
2621 | + responses.calls[0].request.headers["Authorization"], |
2622 | + ) |
2623 | |
2624 | def test_split_assertions(self): |
2625 | result = _split_assertions(self.snap_assert) |
2626 | assert len(result) == 4 |
2627 | |
2628 | # We should have 4 different types of assertion |
2629 | - assert result[0][0] == 'type: account-key' |
2630 | - assert result[1][0] == 'type: account' |
2631 | - assert result[2][0] == 'type: snap-declaration' |
2632 | - assert result[3][0] == 'type: snap-revision' |
2633 | + assert result[0][0] == "type: account-key" |
2634 | + assert result[1][0] == "type: account" |
2635 | + assert result[2][0] == "type: snap-declaration" |
2636 | + assert result[3][0] == "type: snap-revision" |
2637 | |
2638 | @responses.activate |
2639 | def test_add_assertion_to_service(self): |
2640 | - assert_url = urljoin(self.default_gw_url, '/v1/assertions') |
2641 | - responses.add('POST', assert_url, status=201) |
2642 | + assert_url = urljoin(self.default_gw_url, "/v1/assertions") |
2643 | + responses.add("POST", assert_url, status=201) |
2644 | |
2645 | split_assertions = _split_assertions(self.snap_assert) |
2646 | - _add_assertion_to_service(self.store, 'test', split_assertions) |
2647 | + _add_assertion_to_service(self.store, "test", split_assertions) |
2648 | |
2649 | self.assertIn( |
2650 | - 'display-name: Tom Wardill (Ω)', |
2651 | - responses.calls[0].request.body.decode('utf-8'), |
2652 | + "display-name: Tom Wardill (Ω)", |
2653 | + responses.calls[0].request.body.decode("utf-8"), |
2654 | ) |
2655 | self.assertEqual( |
2656 | - 'Basic YWRtaW46dGVzdA==', |
2657 | - responses.calls[0].request.headers['Authorization']) |
2658 | + "Basic YWRtaW46dGVzdA==", |
2659 | + responses.calls[0].request.headers["Authorization"], |
2660 | + ) |
2661 | |
2662 | @responses.activate |
2663 | def test_add_assertion_to_service_unexpected_status_code(self): |
2664 | - assert_url = urljoin(self.default_gw_url, '/v1/assertions') |
2665 | - responses.add('POST', assert_url, status=302) |
2666 | + assert_url = urljoin(self.default_gw_url, "/v1/assertions") |
2667 | + responses.add("POST", assert_url, status=302) |
2668 | |
2669 | split_assertions = _split_assertions(self.snap_assert) |
2670 | self.assertRaises( |
2671 | pushException, |
2672 | - _add_assertion_to_service, self.store, 'test', split_assertions) |
2673 | + _add_assertion_to_service, |
2674 | + self.store, |
2675 | + "test", |
2676 | + split_assertions, |
2677 | + ) |
2678 | |
2679 | @responses.activate |
2680 | def test_add_assertion_to_service_failed(self): |
2681 | - assert_url = urljoin(self.default_gw_url, '/v1/assertions') |
2682 | - responses.add('POST', assert_url, status=500) |
2683 | + assert_url = urljoin(self.default_gw_url, "/v1/assertions") |
2684 | + responses.add("POST", assert_url, status=500) |
2685 | |
2686 | split_assertions = _split_assertions(self.snap_assert) |
2687 | self.assertRaises( |
2688 | pushException, |
2689 | - _add_assertion_to_service, self.store, 'test', split_assertions) |
2690 | + _add_assertion_to_service, |
2691 | + self.store, |
2692 | + "test", |
2693 | + split_assertions, |
2694 | + ) |
2695 | diff --git a/snapstore_client/presentation_helpers.py b/snapstore_client/presentation_helpers.py |
2696 | index 74e47b1..96846b7 100644 |
2697 | --- a/snapstore_client/presentation_helpers.py |
2698 | +++ b/snapstore_client/presentation_helpers.py |
2699 | @@ -17,7 +17,7 @@ def channel_map_string_to_tuple(channel_map_string): |
2700 | |
2701 | 'edge=1' becomes ('edge', 1). |
2702 | """ |
2703 | - parts = channel_map_string.rsplit('=', 1) |
2704 | + parts = channel_map_string.rsplit("=", 1) |
2705 | if len(parts) != 2: |
2706 | raise ValueError("Invalid channel map string: %r" % channel_map_string) |
2707 | channel = parts[0] |
2708 | @@ -30,13 +30,13 @@ def channel_map_string_to_tuple(channel_map_string): |
2709 | |
2710 | def override_to_string(override): |
2711 | """Convert a channel map override into a string presentation.""" |
2712 | - template = '{snap_name} {channel} {architecture}' |
2713 | - if override['revision'] is None: |
2714 | - template += ' is tracking upstream' |
2715 | - if override['upstream_revision'] is not None: |
2716 | - template += ' (revision {upstream_revision:d})' |
2717 | + template = "{snap_name} {channel} {architecture}" |
2718 | + if override["revision"] is None: |
2719 | + template += " is tracking upstream" |
2720 | + if override["upstream_revision"] is not None: |
2721 | + template += " (revision {upstream_revision:d})" |
2722 | else: |
2723 | - template += ' {revision:d}' |
2724 | - if override['upstream_revision'] is not None: |
2725 | - template += ' (upstream {upstream_revision:d})' |
2726 | + template += " {revision:d}" |
2727 | + if override["upstream_revision"] is not None: |
2728 | + template += " (upstream {upstream_revision:d})" |
2729 | return template.format_map(override) |
2730 | diff --git a/snapstore_client/tests/factory.py b/snapstore_client/tests/factory.py |
2731 | index f9d4ba5..f94ab1a 100644 |
2732 | --- a/snapstore_client/tests/factory.py |
2733 | +++ b/snapstore_client/tests/factory.py |
2734 | @@ -30,7 +30,7 @@ def generate_snap_id(): |
2735 | This function does not check for duplicates. |
2736 | |
2737 | """ |
2738 | - return ''.join(random.choice(SNAP_ID_ALPHABET) for _ in range(32)) |
2739 | + return "".join(random.choice(SNAP_ID_ALPHABET) for _ in range(32)) |
2740 | |
2741 | |
2742 | class APIError(Exception): |
2743 | @@ -78,7 +78,7 @@ class APIError(Exception): |
2744 | return len(self._error_list) > 0 |
2745 | |
2746 | def to_dict(self): |
2747 | - return {'error-list': [{'message': m} for m in self._error_list]} |
2748 | + return {"error-list": [{"message": m} for m in self._error_list]} |
2749 | |
2750 | @classmethod |
2751 | def single(cls, message): |
2752 | @@ -90,21 +90,26 @@ class APIError(Exception): |
2753 | |
2754 | |
2755 | class SnapDeviceGateway: |
2756 | - |
2757 | @staticmethod |
2758 | - def Override(snap_id=None, snap_name='special-sauce', revision=1, |
2759 | - upstream_revision=2, channel='stable', architecture='amd64', |
2760 | - series='16'): |
2761 | + def Override( |
2762 | + snap_id=None, |
2763 | + snap_name="special-sauce", |
2764 | + revision=1, |
2765 | + upstream_revision=2, |
2766 | + channel="stable", |
2767 | + architecture="amd64", |
2768 | + series="16", |
2769 | + ): |
2770 | if snap_id is None: |
2771 | snap_id = generate_snap_id() |
2772 | return { |
2773 | - 'snap_id': snap_id, |
2774 | - 'snap_name': snap_name, |
2775 | - 'revision': revision, |
2776 | - 'upstream_revision': upstream_revision, |
2777 | - 'channel': channel, |
2778 | - 'architecture': architecture, |
2779 | - 'series': series, |
2780 | + "snap_id": snap_id, |
2781 | + "snap_name": snap_name, |
2782 | + "revision": revision, |
2783 | + "upstream_revision": upstream_revision, |
2784 | + "channel": channel, |
2785 | + "architecture": architecture, |
2786 | + "series": series, |
2787 | } |
2788 | |
2789 | |
2790 | diff --git a/snapstore_client/tests/matchers.py b/snapstore_client/tests/matchers.py |
2791 | index 29e10b7..baea1a6 100644 |
2792 | --- a/snapstore_client/tests/matchers.py |
2793 | +++ b/snapstore_client/tests/matchers.py |
2794 | @@ -22,19 +22,18 @@ class MacaroonsVerify(Matcher): |
2795 | self.key = key |
2796 | |
2797 | def match(self, macaroons): |
2798 | - mismatch = Contains('root').match(macaroons) |
2799 | + mismatch = Contains("root").match(macaroons) |
2800 | if mismatch is not None: |
2801 | return mismatch |
2802 | - root_macaroon = Macaroon.deserialize(macaroons['root']) |
2803 | - if 'discharge' in macaroons: |
2804 | - discharge_macaroons = [ |
2805 | - Macaroon.deserialize(macaroons['discharge'])] |
2806 | + root_macaroon = Macaroon.deserialize(macaroons["root"]) |
2807 | + if "discharge" in macaroons: |
2808 | + discharge_macaroons = [Macaroon.deserialize(macaroons["discharge"])] |
2809 | else: |
2810 | discharge_macaroons = [] |
2811 | try: |
2812 | Verifier().verify(root_macaroon, self.key, discharge_macaroons) |
2813 | except Exception as e: |
2814 | - return Mismatch('Macaroons do not verify: %s' % e) |
2815 | + return Mismatch("Macaroons do not verify: %s" % e) |
2816 | |
2817 | |
2818 | class MacaroonHeaderVerifies(Matcher): |
2819 | @@ -45,8 +44,9 @@ class MacaroonHeaderVerifies(Matcher): |
2820 | self.key = key |
2821 | |
2822 | def match(self, authz_header): |
2823 | - mismatch = StartsWith('Macaroon ').match(authz_header) |
2824 | + mismatch = StartsWith("Macaroon ").match(authz_header) |
2825 | if mismatch is not None: |
2826 | return mismatch |
2827 | return MacaroonsVerify(self.key).match( |
2828 | - parse_dict_header(authz_header[len('Macaroon '):])) |
2829 | + parse_dict_header(authz_header[len("Macaroon ") :]) |
2830 | + ) |
2831 | diff --git a/snapstore_client/tests/test_cli.py b/snapstore_client/tests/test_cli.py |
2832 | deleted file mode 100644 |
2833 | index a0b8257..0000000 |
2834 | --- a/snapstore_client/tests/test_cli.py |
2835 | +++ /dev/null |
2836 | @@ -1,65 +0,0 @@ |
2837 | -# Copyright 2017 Canonical Ltd. This software is licensed under the |
2838 | -# GNU General Public License version 3 (see the file LICENSE). |
2839 | - |
2840 | -import logging |
2841 | - |
2842 | -import fixtures |
2843 | -from testtools import TestCase |
2844 | - |
2845 | -from snapstore_client import cli |
2846 | - |
2847 | - |
2848 | -class ConfigureLoggingTests(TestCase): |
2849 | - |
2850 | - def setUp(self): |
2851 | - super().setUp() |
2852 | - self.logger = logging.getLogger(__name__) |
2853 | - self.addCleanup( |
2854 | - self._restoreLogger, |
2855 | - self.logger, self.logger.level, list(self.logger.handlers)) |
2856 | - self.stdout = self.useFixture(fixtures.StringStream('stdout')).stream |
2857 | - self.stdout.fileno = lambda: 1 |
2858 | - self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout)) |
2859 | - self.stderr = self.useFixture(fixtures.StringStream('stderr')).stream |
2860 | - self.useFixture(fixtures.MonkeyPatch('sys.stderr', self.stderr)) |
2861 | - |
2862 | - @staticmethod |
2863 | - def _restoreLogger(logger, level, handlers): |
2864 | - logger.setLevel(logger.level) |
2865 | - for handler in list(logger.handlers): |
2866 | - logger.removeHandler(handler) |
2867 | - for handler in handlers: |
2868 | - logger.addHandler(handler) |
2869 | - |
2870 | - def test_log_levels(self): |
2871 | - self.useFixture(fixtures.MonkeyPatch('os.isatty', lambda fd: True)) |
2872 | - cli.configure_logging(__name__) |
2873 | - self.assertEqual(logging.INFO, self.logger.level) |
2874 | - self.logger.debug('Debug') |
2875 | - self.logger.info('Info') |
2876 | - self.logger.warning('Warning: %s', 'smoke') |
2877 | - self.logger.error('Error: %s', 'fire') |
2878 | - self.stdout.seek(0) |
2879 | - self.assertEqual('Info\nWarning: smoke\n', self.stdout.read()) |
2880 | - self.stderr.seek(0) |
2881 | - self.assertEqual('\033[0;31mError: fire\033[0m\n', self.stderr.read()) |
2882 | - |
2883 | - def test_requests_log_level_default(self): |
2884 | - cli.configure_logging(__name__) |
2885 | - self.assertEqual(logging.WARNING, logging.getLogger('requests').level) |
2886 | - |
2887 | - def test_requests_log_level_debug(self): |
2888 | - cli.configure_logging(__name__, logging.DEBUG) |
2889 | - self.assertEqual(logging.DEBUG, logging.getLogger('requests').level) |
2890 | - |
2891 | - def test_requests_log_level_error(self): |
2892 | - cli.configure_logging(__name__, logging.ERROR) |
2893 | - self.assertEqual(logging.ERROR, logging.getLogger('requests').level) |
2894 | - |
2895 | - def test_no_tty(self): |
2896 | - self.useFixture(fixtures.MonkeyPatch('os.isatty', lambda fd: False)) |
2897 | - self.useFixture(fixtures.EnvironmentVariable('TERM', 'xterm')) |
2898 | - cli.configure_logging(__name__) |
2899 | - self.logger.error('Error: %s', 'fire') |
2900 | - self.stderr.seek(0) |
2901 | - self.assertEqual('Error: fire\n', self.stderr.read()) |
2902 | diff --git a/snapstore_client/tests/test_config.py b/snapstore_client/tests/test_config.py |
2903 | index 320b51c..7243554 100644 |
2904 | --- a/snapstore_client/tests/test_config.py |
2905 | +++ b/snapstore_client/tests/test_config.py |
2906 | @@ -14,10 +14,9 @@ from snapstore_client.tests.testfixtures import ( |
2907 | |
2908 | |
2909 | class ConfigTests(TestCase): |
2910 | - |
2911 | def test_no_config(self): |
2912 | xdg_path = self.useFixture(XDGConfigDirFixture()).path |
2913 | - config_ini = os.path.join(xdg_path, Config.xdg_name, 'config.ini') |
2914 | + config_ini = os.path.join(xdg_path, Config.xdg_name, "config.ini") |
2915 | self.assertFalse(os.path.exists(config_ini)) |
2916 | Config() |
2917 | |
2918 | @@ -28,8 +27,7 @@ class ConfigTests(TestCase): |
2919 | |
2920 | def test_save(self): |
2921 | xdg_path = self.useFixture(XDGConfigDirFixture()).path |
2922 | - config_ini = os.path.join( |
2923 | - xdg_path, 'snap-store-proxy-client', 'config.ini') |
2924 | + config_ini = os.path.join(xdg_path, "snap-store-proxy-client", "config.ini") |
2925 | self.assertFalse(os.path.exists(config_ini)) |
2926 | |
2927 | cfg = Config() |
2928 | @@ -39,11 +37,16 @@ class ConfigTests(TestCase): |
2929 | self.assertTrue(os.path.exists(config_ini)) |
2930 | with open(config_ini) as f: |
2931 | content = f.read() |
2932 | - self.assertEqual(content, dedent('''\ |
2933 | + self.assertEqual( |
2934 | + content, |
2935 | + dedent( |
2936 | + """\ |
2937 | [s] |
2938 | k = v |
2939 | |
2940 | - ''')) |
2941 | + """ |
2942 | + ), |
2943 | + ) |
2944 | |
2945 | def test_get_missing(self): |
2946 | self.useFixture(XDGConfigDirFixture()) |
2947 | @@ -74,4 +77,5 @@ class ConfigTests(TestCase): |
2948 | |
2949 | def test_suite(): |
2950 | from unittest import TestLoader |
2951 | + |
2952 | return TestLoader().loadTestsFromName(__name__) |
2953 | diff --git a/snapstore_client/tests/test_presentation_helpers.py b/snapstore_client/tests/test_presentation_helpers.py |
2954 | index b46345d..7399f91 100644 |
2955 | --- a/snapstore_client/tests/test_presentation_helpers.py |
2956 | +++ b/snapstore_client/tests/test_presentation_helpers.py |
2957 | @@ -12,95 +12,125 @@ from snapstore_client.presentation_helpers import ( |
2958 | class ChannelMapStringToTupleScenarioTests(WithScenarios, TestCase): |
2959 | |
2960 | scenarios = [ |
2961 | - ('with risk', { |
2962 | - 'channel_map': 'stable=1', |
2963 | - 'terms': ('stable', 1), |
2964 | - }), |
2965 | - ('with track and risk', { |
2966 | - 'channel_map': '2.1/stable=2', |
2967 | - 'terms': ('2.1/stable', 2), |
2968 | - }), |
2969 | - ('with risk and branch', { |
2970 | - 'channel_map': 'stable/hot-fix=42', |
2971 | - 'terms': ('stable/hot-fix', 42), |
2972 | - }), |
2973 | - ('with track, risk and branch', { |
2974 | - 'channel_map': '2.1/stable/hot-fix=123', |
2975 | - 'terms': ('2.1/stable/hot-fix', 123), |
2976 | - }), |
2977 | + ( |
2978 | + "with risk", |
2979 | + { |
2980 | + "channel_map": "stable=1", |
2981 | + "terms": ("stable", 1), |
2982 | + }, |
2983 | + ), |
2984 | + ( |
2985 | + "with track and risk", |
2986 | + { |
2987 | + "channel_map": "2.1/stable=2", |
2988 | + "terms": ("2.1/stable", 2), |
2989 | + }, |
2990 | + ), |
2991 | + ( |
2992 | + "with risk and branch", |
2993 | + { |
2994 | + "channel_map": "stable/hot-fix=42", |
2995 | + "terms": ("stable/hot-fix", 42), |
2996 | + }, |
2997 | + ), |
2998 | + ( |
2999 | + "with track, risk and branch", |
3000 | + { |
3001 | + "channel_map": "2.1/stable/hot-fix=123", |
3002 | + "terms": ("2.1/stable/hot-fix", 123), |
3003 | + }, |
3004 | + ), |
3005 | ] |
3006 | |
3007 | def test_run_scenario(self): |
3008 | - self.assertEqual( |
3009 | - self.terms, channel_map_string_to_tuple(self.channel_map)) |
3010 | + self.assertEqual(self.terms, channel_map_string_to_tuple(self.channel_map)) |
3011 | |
3012 | |
3013 | class ChannelMapStringToTupleErrorTests(WithScenarios, TestCase): |
3014 | |
3015 | scenarios = [ |
3016 | - ('missing revision', { |
3017 | - 'channel_map': 'stable', |
3018 | - 'error_message': "Invalid channel map string: 'stable'", |
3019 | - }), |
3020 | - ('non-integer revision', { |
3021 | - 'channel_map': 'stable=nonsense', |
3022 | - 'error_message': "Invalid revision string: 'nonsense'", |
3023 | - }), |
3024 | + ( |
3025 | + "missing revision", |
3026 | + { |
3027 | + "channel_map": "stable", |
3028 | + "error_message": "Invalid channel map string: 'stable'", |
3029 | + }, |
3030 | + ), |
3031 | + ( |
3032 | + "non-integer revision", |
3033 | + { |
3034 | + "channel_map": "stable=nonsense", |
3035 | + "error_message": "Invalid revision string: 'nonsense'", |
3036 | + }, |
3037 | + ), |
3038 | ] |
3039 | |
3040 | def test_run_scenario(self): |
3041 | error = self.assertRaises( |
3042 | - ValueError, channel_map_string_to_tuple, self.channel_map) |
3043 | + ValueError, channel_map_string_to_tuple, self.channel_map |
3044 | + ) |
3045 | self.assertEqual(self.error_message, str(error)) |
3046 | |
3047 | |
3048 | class OverrideToStringTests(WithScenarios, TestCase): |
3049 | |
3050 | scenarios = [ |
3051 | - ('without revision or upstream revision', { |
3052 | - 'override': { |
3053 | - 'snap_id': 'dummy', |
3054 | - 'snap_name': 'mysnap', |
3055 | - 'revision': None, |
3056 | - 'upstream_revision': None, |
3057 | - 'channel': 'stable', |
3058 | - 'architecture': 'amd64', |
3059 | + ( |
3060 | + "without revision or upstream revision", |
3061 | + { |
3062 | + "override": { |
3063 | + "snap_id": "dummy", |
3064 | + "snap_name": "mysnap", |
3065 | + "revision": None, |
3066 | + "upstream_revision": None, |
3067 | + "channel": "stable", |
3068 | + "architecture": "amd64", |
3069 | + }, |
3070 | + "string": "mysnap stable amd64 is tracking upstream", |
3071 | }, |
3072 | - 'string': 'mysnap stable amd64 is tracking upstream', |
3073 | - }), |
3074 | - ('without revision but with upstream revision', { |
3075 | - 'override': { |
3076 | - 'snap_id': 'dummy', |
3077 | - 'snap_name': 'mysnap', |
3078 | - 'revision': None, |
3079 | - 'upstream_revision': 2, |
3080 | - 'channel': 'stable', |
3081 | - 'architecture': 'amd64', |
3082 | + ), |
3083 | + ( |
3084 | + "without revision but with upstream revision", |
3085 | + { |
3086 | + "override": { |
3087 | + "snap_id": "dummy", |
3088 | + "snap_name": "mysnap", |
3089 | + "revision": None, |
3090 | + "upstream_revision": 2, |
3091 | + "channel": "stable", |
3092 | + "architecture": "amd64", |
3093 | + }, |
3094 | + "string": "mysnap stable amd64 is tracking upstream (revision 2)", |
3095 | }, |
3096 | - 'string': 'mysnap stable amd64 is tracking upstream (revision 2)', |
3097 | - }), |
3098 | - ('with revision but without upstream revision', { |
3099 | - 'override': { |
3100 | - 'snap_id': 'dummy', |
3101 | - 'snap_name': 'mysnap', |
3102 | - 'revision': 1, |
3103 | - 'upstream_revision': None, |
3104 | - 'channel': 'stable', |
3105 | - 'architecture': 'amd64', |
3106 | + ), |
3107 | + ( |
3108 | + "with revision but without upstream revision", |
3109 | + { |
3110 | + "override": { |
3111 | + "snap_id": "dummy", |
3112 | + "snap_name": "mysnap", |
3113 | + "revision": 1, |
3114 | + "upstream_revision": None, |
3115 | + "channel": "stable", |
3116 | + "architecture": "amd64", |
3117 | + }, |
3118 | + "string": "mysnap stable amd64 1", |
3119 | }, |
3120 | - 'string': 'mysnap stable amd64 1', |
3121 | - }), |
3122 | - ('with revision and upstream revision', { |
3123 | - 'override': { |
3124 | - 'snap_id': 'dummy', |
3125 | - 'snap_name': 'mysnap', |
3126 | - 'revision': 1, |
3127 | - 'upstream_revision': 2, |
3128 | - 'channel': 'stable', |
3129 | - 'architecture': 'amd64', |
3130 | + ), |
3131 | + ( |
3132 | + "with revision and upstream revision", |
3133 | + { |
3134 | + "override": { |
3135 | + "snap_id": "dummy", |
3136 | + "snap_name": "mysnap", |
3137 | + "revision": 1, |
3138 | + "upstream_revision": 2, |
3139 | + "channel": "stable", |
3140 | + "architecture": "amd64", |
3141 | + }, |
3142 | + "string": "mysnap stable amd64 1 (upstream 2)", |
3143 | }, |
3144 | - 'string': 'mysnap stable amd64 1 (upstream 2)', |
3145 | - }), |
3146 | + ), |
3147 | ] |
3148 | |
3149 | def test_run_scenario(self): |
3150 | diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py |
3151 | index c9c743c..ec2019d 100644 |
3152 | --- a/snapstore_client/tests/test_webservices.py |
3153 | +++ b/snapstore_client/tests/test_webservices.py |
3154 | @@ -20,127 +20,165 @@ from snapstore_client.tests import ( |
3155 | testfixtures, |
3156 | ) |
3157 | |
3158 | -if sys.version < '3.6': |
3159 | +if sys.version < "3.6": |
3160 | import sha3 # noqa |
3161 | |
3162 | |
3163 | class WebservicesTests(TestCase): |
3164 | - |
3165 | def setUp(self): |
3166 | super().setUp() |
3167 | self.config = self.useFixture(testfixtures.ConfigFixture()) |
3168 | |
3169 | @responses.activate |
3170 | def test_issue_store_admin_success(self): |
3171 | - gw_url = 'http://store.local/' |
3172 | - issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
3173 | + gw_url = "http://store.local/" |
3174 | + issue_store_admin_url = urljoin(gw_url, "/v2/auth/issue-store-admin") |
3175 | responses.add( |
3176 | - 'POST', issue_store_admin_url, status=200, |
3177 | - json={'macaroon': 'dummy'}) |
3178 | + "POST", issue_store_admin_url, status=200, json={"macaroon": "dummy"} |
3179 | + ) |
3180 | |
3181 | - self.assertEqual('dummy', webservices.issue_store_admin(gw_url)) |
3182 | + self.assertEqual("dummy", webservices.issue_store_admin(gw_url)) |
3183 | |
3184 | @responses.activate |
3185 | def test_issue_store_admin_error(self): |
3186 | logger = self.useFixture(fixtures.FakeLogger()) |
3187 | - gw_url = 'http://store.local/' |
3188 | - issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin') |
3189 | + gw_url = "http://store.local/" |
3190 | + issue_store_admin_url = urljoin(gw_url, "/v2/auth/issue-store-admin") |
3191 | responses.add( |
3192 | - 'POST', issue_store_admin_url, status=400, |
3193 | - json=factory.APIError.single('Something went wrong').to_dict()) |
3194 | + "POST", |
3195 | + issue_store_admin_url, |
3196 | + status=400, |
3197 | + json=factory.APIError.single("Something went wrong").to_dict(), |
3198 | + ) |
3199 | |
3200 | - self.assertRaises( |
3201 | - HTTPError, webservices.issue_store_admin, gw_url) |
3202 | + self.assertRaises(HTTPError, webservices.issue_store_admin, gw_url) |
3203 | self.assertEqual( |
3204 | - 'Failed to issue store_admin macaroon:\nSomething went wrong\n', |
3205 | - logger.output) |
3206 | + "Failed to issue store_admin macaroon:\nSomething went wrong\n", |
3207 | + logger.output, |
3208 | + ) |
3209 | |
3210 | @responses.activate |
3211 | def test_get_sso_discharge_success(self): |
3212 | - sso_url = 'http://sso.local/' |
3213 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
3214 | + sso_url = "http://sso.local/" |
3215 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
3216 | responses.add( |
3217 | - 'POST', discharge_url, status=200, |
3218 | - json={'discharge_macaroon': 'dummy'}) |
3219 | + "POST", discharge_url, status=200, json={"discharge_macaroon": "dummy"} |
3220 | + ) |
3221 | |
3222 | self.assertEqual( |
3223 | - 'dummy', |
3224 | + "dummy", |
3225 | webservices.get_sso_discharge( |
3226 | - sso_url, 'user@example.org', 'secret', 'caveat')) |
3227 | + sso_url, "user@example.org", "secret", "caveat" |
3228 | + ), |
3229 | + ) |
3230 | request = responses.calls[0].request |
3231 | - self.assertEqual('application/json', request.headers['Content-Type']) |
3232 | - self.assertEqual({ |
3233 | - 'email': 'user@example.org', |
3234 | - 'password': 'secret', |
3235 | - 'caveat_id': 'caveat', |
3236 | - }, json.loads(request.body.decode())) |
3237 | + self.assertEqual("application/json", request.headers["Content-Type"]) |
3238 | + self.assertEqual( |
3239 | + { |
3240 | + "email": "user@example.org", |
3241 | + "password": "secret", |
3242 | + "caveat_id": "caveat", |
3243 | + }, |
3244 | + json.loads(request.body.decode()), |
3245 | + ) |
3246 | |
3247 | @responses.activate |
3248 | def test_get_sso_discharge_success_with_otp(self): |
3249 | - sso_url = 'http://sso.local/' |
3250 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
3251 | + sso_url = "http://sso.local/" |
3252 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
3253 | responses.add( |
3254 | - 'POST', discharge_url, status=200, |
3255 | - json={'discharge_macaroon': 'dummy'}) |
3256 | + "POST", discharge_url, status=200, json={"discharge_macaroon": "dummy"} |
3257 | + ) |
3258 | |
3259 | self.assertEqual( |
3260 | - 'dummy', |
3261 | + "dummy", |
3262 | webservices.get_sso_discharge( |
3263 | - sso_url, 'user@example.org', 'secret', 'caveat', |
3264 | - one_time_password='123456')) |
3265 | + sso_url, |
3266 | + "user@example.org", |
3267 | + "secret", |
3268 | + "caveat", |
3269 | + one_time_password="123456", |
3270 | + ), |
3271 | + ) |
3272 | request = responses.calls[0].request |
3273 | - self.assertEqual('application/json', request.headers['Content-Type']) |
3274 | - self.assertEqual({ |
3275 | - 'email': 'user@example.org', |
3276 | - 'password': 'secret', |
3277 | - 'caveat_id': 'caveat', |
3278 | - 'otp': '123456', |
3279 | - }, json.loads(request.body.decode())) |
3280 | + self.assertEqual("application/json", request.headers["Content-Type"]) |
3281 | + self.assertEqual( |
3282 | + { |
3283 | + "email": "user@example.org", |
3284 | + "password": "secret", |
3285 | + "caveat_id": "caveat", |
3286 | + "otp": "123456", |
3287 | + }, |
3288 | + json.loads(request.body.decode()), |
3289 | + ) |
3290 | |
3291 | @responses.activate |
3292 | def test_get_sso_discharge_twofactor_required(self): |
3293 | - sso_url = 'http://sso.local/' |
3294 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
3295 | + sso_url = "http://sso.local/" |
3296 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
3297 | responses.add( |
3298 | - 'POST', discharge_url, status=401, |
3299 | - json={'error_list': [{'code': 'twofactor-required'}]}) |
3300 | + "POST", |
3301 | + discharge_url, |
3302 | + status=401, |
3303 | + json={"error_list": [{"code": "twofactor-required"}]}, |
3304 | + ) |
3305 | |
3306 | self.assertRaises( |
3307 | exceptions.StoreTwoFactorAuthenticationRequired, |
3308 | webservices.get_sso_discharge, |
3309 | - sso_url, 'user@example.org', 'secret', 'caveat') |
3310 | + sso_url, |
3311 | + "user@example.org", |
3312 | + "secret", |
3313 | + "caveat", |
3314 | + ) |
3315 | |
3316 | @responses.activate |
3317 | def test_get_sso_discharge_structured_error(self): |
3318 | - sso_url = 'http://sso.local/' |
3319 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
3320 | + sso_url = "http://sso.local/" |
3321 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
3322 | responses.add( |
3323 | - 'POST', discharge_url, status=400, |
3324 | - json={'error_list': [{'code': 'invalid-request', |
3325 | - 'message': 'Something went wrong'}]}) |
3326 | + "POST", |
3327 | + discharge_url, |
3328 | + status=400, |
3329 | + json={ |
3330 | + "error_list": [ |
3331 | + {"code": "invalid-request", "message": "Something went wrong"} |
3332 | + ] |
3333 | + }, |
3334 | + ) |
3335 | |
3336 | e = self.assertRaises( |
3337 | - exceptions.StoreAuthenticationError, webservices.get_sso_discharge, |
3338 | - sso_url, 'user@example.org', 'secret', 'caveat') |
3339 | - self.assertEqual('Something went wrong', e.message) |
3340 | + exceptions.StoreAuthenticationError, |
3341 | + webservices.get_sso_discharge, |
3342 | + sso_url, |
3343 | + "user@example.org", |
3344 | + "secret", |
3345 | + "caveat", |
3346 | + ) |
3347 | + self.assertEqual("Something went wrong", e.message) |
3348 | |
3349 | @responses.activate |
3350 | def test_get_sso_discharge_unstructured_error(self): |
3351 | logger = self.useFixture(fixtures.FakeLogger()) |
3352 | - sso_url = 'http://sso.local/' |
3353 | - discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge') |
3354 | - responses.add( |
3355 | - 'POST', discharge_url, status=503, body='Try again later.') |
3356 | + sso_url = "http://sso.local/" |
3357 | + discharge_url = urljoin(sso_url, "/api/v2/tokens/discharge") |
3358 | + responses.add("POST", discharge_url, status=503, body="Try again later.") |
3359 | |
3360 | self.assertRaises( |
3361 | - HTTPError, webservices.get_sso_discharge, |
3362 | - sso_url, 'user@example.org', 'secret', 'caveat') |
3363 | + HTTPError, |
3364 | + webservices.get_sso_discharge, |
3365 | + sso_url, |
3366 | + "user@example.org", |
3367 | + "secret", |
3368 | + "caveat", |
3369 | + ) |
3370 | self.assertEqual( |
3371 | - 'Failed to get SSO discharge:\n' |
3372 | - '====================\n' |
3373 | - 'Try again later.\n' |
3374 | - '====================\n', |
3375 | - logger.output) |
3376 | + "Failed to get SSO discharge:\n" |
3377 | + "====================\n" |
3378 | + "Try again later.\n" |
3379 | + "====================\n", |
3380 | + logger.output, |
3381 | + ) |
3382 | |
3383 | @responses.activate |
3384 | def test_get_overrides_success(self): |
3385 | @@ -148,33 +186,34 @@ class WebservicesTests(TestCase): |
3386 | overrides = [factory.SnapDeviceGateway.Override()] |
3387 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
3388 | # exists. |
3389 | - store = config.Config().store_section('default') |
3390 | - overrides_url = urljoin( |
3391 | - store.get('gw_url'), '/v2/metadata/overrides/mysnap') |
3392 | - responses.add('GET', overrides_url, status=200, json=overrides) |
3393 | + store = config.Config().store_section("default") |
3394 | + overrides_url = urljoin(store.get("gw_url"), "/v2/metadata/overrides/mysnap") |
3395 | + responses.add("GET", overrides_url, status=200, json=overrides) |
3396 | |
3397 | - self.assertEqual(overrides, webservices.get_overrides( |
3398 | - store, 'mysnap')) |
3399 | + self.assertEqual(overrides, webservices.get_overrides(store, "mysnap")) |
3400 | request = responses.calls[0].request |
3401 | self.assertThat( |
3402 | - request.headers['Authorization'], |
3403 | - matchers.MacaroonHeaderVerifies(self.config.key)) |
3404 | - self.assertNotIn('Failed to get overrides:', logger.output) |
3405 | + request.headers["Authorization"], |
3406 | + matchers.MacaroonHeaderVerifies(self.config.key), |
3407 | + ) |
3408 | + self.assertNotIn("Failed to get overrides:", logger.output) |
3409 | |
3410 | @responses.activate |
3411 | def test_get_overrides_error(self): |
3412 | logger = self.useFixture(fixtures.FakeLogger()) |
3413 | - store = config.Config().store_section('default') |
3414 | - overrides_url = urljoin( |
3415 | - store.get('gw_url'), '/v2/metadata/overrides/mysnap') |
3416 | + store = config.Config().store_section("default") |
3417 | + overrides_url = urljoin(store.get("gw_url"), "/v2/metadata/overrides/mysnap") |
3418 | responses.add( |
3419 | - 'GET', overrides_url, status=400, |
3420 | - json=factory.APIError.single('Something went wrong').to_dict()) |
3421 | + "GET", |
3422 | + overrides_url, |
3423 | + status=400, |
3424 | + json=factory.APIError.single("Something went wrong").to_dict(), |
3425 | + ) |
3426 | |
3427 | - self.assertRaises( |
3428 | - HTTPError, webservices.get_overrides, store, 'mysnap') |
3429 | + self.assertRaises(HTTPError, webservices.get_overrides, store, "mysnap") |
3430 | self.assertEqual( |
3431 | - 'Failed to get overrides:\nSomething went wrong\n', logger.output) |
3432 | + "Failed to get overrides:\nSomething went wrong\n", logger.output |
3433 | + ) |
3434 | |
3435 | @responses.activate |
3436 | def test_set_overrides_success(self): |
3437 | @@ -182,28 +221,41 @@ class WebservicesTests(TestCase): |
3438 | override = factory.SnapDeviceGateway.Override() |
3439 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
3440 | # exists. |
3441 | - store = config.Config().store_section('default') |
3442 | - overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides') |
3443 | - responses.add('POST', overrides_url, status=200, json=[override]) |
3444 | - |
3445 | - self.assertEqual([override], webservices.set_overrides( |
3446 | - store, [{ |
3447 | - 'snap_name': override['snap_name'], |
3448 | - 'revision': override['revision'], |
3449 | - 'channel': override['channel'], |
3450 | - 'series': override['series'], |
3451 | - }])) |
3452 | + store = config.Config().store_section("default") |
3453 | + overrides_url = urljoin(store.get("gw_url"), "/v2/metadata/overrides") |
3454 | + responses.add("POST", overrides_url, status=200, json=[override]) |
3455 | + |
3456 | + self.assertEqual( |
3457 | + [override], |
3458 | + webservices.set_overrides( |
3459 | + store, |
3460 | + [ |
3461 | + { |
3462 | + "snap_name": override["snap_name"], |
3463 | + "revision": override["revision"], |
3464 | + "channel": override["channel"], |
3465 | + "series": override["series"], |
3466 | + } |
3467 | + ], |
3468 | + ), |
3469 | + ) |
3470 | request = responses.calls[0].request |
3471 | self.assertThat( |
3472 | - request.headers['Authorization'], |
3473 | - matchers.MacaroonHeaderVerifies(self.config.key)) |
3474 | - self.assertEqual([{ |
3475 | - 'snap_name': override['snap_name'], |
3476 | - 'revision': override['revision'], |
3477 | - 'channel': override['channel'], |
3478 | - 'series': override['series'], |
3479 | - }], json.loads(request.body.decode())) |
3480 | - self.assertNotIn('Failed to set override:', logger.output) |
3481 | + request.headers["Authorization"], |
3482 | + matchers.MacaroonHeaderVerifies(self.config.key), |
3483 | + ) |
3484 | + self.assertEqual( |
3485 | + [ |
3486 | + { |
3487 | + "snap_name": override["snap_name"], |
3488 | + "revision": override["revision"], |
3489 | + "channel": override["channel"], |
3490 | + "series": override["series"], |
3491 | + } |
3492 | + ], |
3493 | + json.loads(request.body.decode()), |
3494 | + ) |
3495 | + self.assertNotIn("Failed to set override:", logger.output) |
3496 | |
3497 | @responses.activate |
3498 | def test_set_overrides_error(self): |
3499 | @@ -211,18 +263,27 @@ class WebservicesTests(TestCase): |
3500 | override = factory.SnapDeviceGateway.Override() |
3501 | # XXX cjwatson 2017-06-26: Use acceptable-generated double once it |
3502 | # exists. |
3503 | - store = config.Config().store_section('default') |
3504 | - overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides') |
3505 | + store = config.Config().store_section("default") |
3506 | + overrides_url = urljoin(store.get("gw_url"), "/v2/metadata/overrides") |
3507 | responses.add( |
3508 | - 'POST', overrides_url, status=400, |
3509 | - json=factory.APIError.single('Something went wrong').to_dict()) |
3510 | - |
3511 | - self.assertRaises(HTTPError, lambda: webservices.set_overrides( |
3512 | - store, { |
3513 | - 'snap_name': override['snap_name'], |
3514 | - 'revision': override['revision'], |
3515 | - 'channel': override['channel'], |
3516 | - 'series': override['series'], |
3517 | - })) |
3518 | + "POST", |
3519 | + overrides_url, |
3520 | + status=400, |
3521 | + json=factory.APIError.single("Something went wrong").to_dict(), |
3522 | + ) |
3523 | + |
3524 | + self.assertRaises( |
3525 | + HTTPError, |
3526 | + lambda: webservices.set_overrides( |
3527 | + store, |
3528 | + { |
3529 | + "snap_name": override["snap_name"], |
3530 | + "revision": override["revision"], |
3531 | + "channel": override["channel"], |
3532 | + "series": override["series"], |
3533 | + }, |
3534 | + ), |
3535 | + ) |
3536 | self.assertEqual( |
3537 | - 'Failed to set override:\nSomething went wrong\n', logger.output) |
3538 | + "Failed to set override:\nSomething went wrong\n", logger.output |
3539 | + ) |
3540 | diff --git a/snapstore_client/tests/testfixtures.py b/snapstore_client/tests/testfixtures.py |
3541 | index 10f714e..68daabf 100644 |
3542 | --- a/snapstore_client/tests/testfixtures.py |
3543 | +++ b/snapstore_client/tests/testfixtures.py |
3544 | @@ -14,11 +14,11 @@ from pymacaroons import Macaroon |
3545 | |
3546 | |
3547 | class NowFixture(MonkeyPatch): |
3548 | - |
3549 | def __init__(self): |
3550 | self.now = datetime.datetime.now() |
3551 | super().__init__( |
3552 | - 'datetime.datetime', types.SimpleNamespace(now=lambda: self.now)) |
3553 | + "datetime.datetime", types.SimpleNamespace(now=lambda: self.now) |
3554 | + ) |
3555 | |
3556 | |
3557 | class XDGConfigDirFixture(Fixture): |
3558 | @@ -28,42 +28,43 @@ class XDGConfigDirFixture(Fixture): |
3559 | |
3560 | def _setUp(self): |
3561 | self.path = self.useFixture(TempDir()).path |
3562 | - self.useFixture(MonkeyPatch( |
3563 | - 'xdg.BaseDirectory.xdg_config_home', self.path)) |
3564 | - self.useFixture(MonkeyPatch( |
3565 | - 'xdg.BaseDirectory.xdg_config_dirs', [self.path])) |
3566 | + self.useFixture(MonkeyPatch("xdg.BaseDirectory.xdg_config_home", self.path)) |
3567 | + self.useFixture(MonkeyPatch("xdg.BaseDirectory.xdg_config_dirs", [self.path])) |
3568 | |
3569 | |
3570 | class ConfigFixture(Fixture): |
3571 | - |
3572 | def __init__(self, empty=False): |
3573 | super().__init__() |
3574 | self.empty = empty |
3575 | - self.gw_url = 'http://store.local/' |
3576 | - self.sso_url = 'http://sso.local/' |
3577 | - self.key = 'random-key' |
3578 | + self.gw_url = "http://store.local/" |
3579 | + self.sso_url = "http://sso.local/" |
3580 | + self.key = "random-key" |
3581 | self.root = Macaroon(key=self.key) |
3582 | - self.root.add_third_party_caveat( |
3583 | - 'login.example.com', 'sso-key', 'payload') |
3584 | + self.root.add_third_party_caveat("login.example.com", "sso-key", "payload") |
3585 | self.unbound_discharge = Macaroon( |
3586 | - location='login.example.com', identifier='payload', key='sso-key') |
3587 | + location="login.example.com", identifier="payload", key="sso-key" |
3588 | + ) |
3589 | |
3590 | def _setUp(self): |
3591 | xdg_config_path = self.useFixture(XDGConfigDirFixture()).path |
3592 | - app_config_path = os.path.join( |
3593 | - xdg_config_path, 'snap-store-proxy-client') |
3594 | + app_config_path = os.path.join(xdg_config_path, "snap-store-proxy-client") |
3595 | os.makedirs(app_config_path) |
3596 | if self.empty: |
3597 | return |
3598 | - with open(os.path.join(app_config_path, 'config.ini'), 'w') as f: |
3599 | - f.write(dedent(""" |
3600 | + with open(os.path.join(app_config_path, "config.ini"), "w") as f: |
3601 | + f.write( |
3602 | + dedent( |
3603 | + """ |
3604 | [store:default] |
3605 | gw_url = {gw_url} |
3606 | sso_url = {sso_url} |
3607 | root = {root} |
3608 | unbound_discharge = {unbound_discharge} |
3609 | - """).format( |
3610 | - gw_url=self.gw_url, sso_url=self.sso_url, |
3611 | - root=self.root.serialize(), |
3612 | - unbound_discharge=self.unbound_discharge.serialize(), |
3613 | - )) |
3614 | + """ |
3615 | + ).format( |
3616 | + gw_url=self.gw_url, |
3617 | + sso_url=self.sso_url, |
3618 | + root=self.root.serialize(), |
3619 | + unbound_discharge=self.unbound_discharge.serialize(), |
3620 | + ) |
3621 | + ) |
3622 | diff --git a/snapstore_client/utils.py b/snapstore_client/utils.py |
3623 | index 461a1c3..1922e60 100644 |
3624 | --- a/snapstore_client/utils.py |
3625 | +++ b/snapstore_client/utils.py |
3626 | @@ -4,23 +4,25 @@ logger = logging.getLogger(__name__) |
3627 | |
3628 | |
3629 | def _log_credentials_error(e): |
3630 | - logger.error('%s', e) |
3631 | + logger.error("%s", e) |
3632 | logger.error('Try to "snap-store-proxy-client login" again.') |
3633 | |
3634 | |
3635 | def _log_authorized_error(): |
3636 | - logger.error(("Perhaps you have not been registered as an " |
3637 | - "admin with the proxy.")) |
3638 | + logger.error( |
3639 | + ("Perhaps you have not been registered as an " "admin with the proxy.") |
3640 | + ) |
3641 | logger.error("Try 'snap-proxy add-admin' on the proxy host.") |
3642 | |
3643 | |
3644 | def _check_default_store(cfg): |
3645 | """Load the default store from the config.""" |
3646 | - store = cfg.store_section('default') |
3647 | + store = cfg.store_section("default") |
3648 | # If the gw URL is configured then everything else should be too. |
3649 | - if not store.get('gw_url'): |
3650 | + if not store.get("gw_url"): |
3651 | logger.error( |
3652 | - 'No store configuration found. ' |
3653 | - 'Have you run "snap-store-proxy-client login"?') |
3654 | + "No store configuration found. " |
3655 | + 'Have you run "snap-store-proxy-client login"?' |
3656 | + ) |
3657 | return None |
3658 | return store |
3659 | diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py |
3660 | index 3ebf149..687d67f 100644 |
3661 | --- a/snapstore_client/webservices.py |
3662 | +++ b/snapstore_client/webservices.py |
3663 | @@ -17,8 +17,7 @@ logger = logging.getLogger(__name__) |
3664 | |
3665 | def issue_store_admin(gw_url): |
3666 | """Ask the store to issue a store_admin macaroon.""" |
3667 | - issue_store_admin_url = urllib.parse.urljoin( |
3668 | - gw_url, '/v2/auth/issue-store-admin') |
3669 | + issue_store_admin_url = urllib.parse.urljoin(gw_url, "/v2/auth/issue-store-admin") |
3670 | try: |
3671 | resp = requests.post(issue_store_admin_url) |
3672 | except requests.exceptions.ConnectionError: |
3673 | @@ -26,94 +25,93 @@ def issue_store_admin(gw_url): |
3674 | except requests.exceptions.RequestException: |
3675 | raise exceptions.InvalidStoreURL(gw_url) |
3676 | if resp.status_code == 404: |
3677 | - _print_error_message('issue store_admin macaroon', resp) |
3678 | + _print_error_message("issue store_admin macaroon", resp) |
3679 | raise exceptions.InvalidStoreURL(gw_url) |
3680 | elif resp.status_code != 200: |
3681 | - _print_error_message('issue store_admin macaroon', resp) |
3682 | + _print_error_message("issue store_admin macaroon", resp) |
3683 | resp.raise_for_status() |
3684 | - return resp.json()['macaroon'] |
3685 | + return resp.json()["macaroon"] |
3686 | |
3687 | |
3688 | -def get_sso_discharge(sso_url, email, password, caveat_id, |
3689 | - one_time_password=None): |
3690 | - discharge_url = urllib.parse.urljoin( |
3691 | - sso_url, '/api/v2/tokens/discharge') |
3692 | - data = {'email': email, 'password': password, 'caveat_id': caveat_id} |
3693 | +def get_sso_discharge(sso_url, email, password, caveat_id, one_time_password=None): |
3694 | + discharge_url = urllib.parse.urljoin(sso_url, "/api/v2/tokens/discharge") |
3695 | + data = {"email": email, "password": password, "caveat_id": caveat_id} |
3696 | if one_time_password is not None: |
3697 | - data['otp'] = one_time_password |
3698 | + data["otp"] = one_time_password |
3699 | resp = requests.post( |
3700 | - discharge_url, headers={'Accept': 'application/json'}, json=data) |
3701 | + discharge_url, headers={"Accept": "application/json"}, json=data |
3702 | + ) |
3703 | if not resp.ok: |
3704 | try: |
3705 | - error_list = resp.json().get('error_list', []) |
3706 | + error_list = resp.json().get("error_list", []) |
3707 | if resp.status_code == 401: |
3708 | for error in error_list: |
3709 | - if error.get('code') == 'twofactor-required': |
3710 | + if error.get("code") == "twofactor-required": |
3711 | raise exceptions.StoreTwoFactorAuthenticationRequired() |
3712 | if error_list: |
3713 | # Only bother about the first error. |
3714 | error = error_list[0] |
3715 | raise exceptions.StoreAuthenticationError( |
3716 | - error['message'], extra=error.get('extra')) |
3717 | + error["message"], extra=error.get("extra") |
3718 | + ) |
3719 | except json.JSONDecodeError: |
3720 | pass |
3721 | - _print_error_message('get SSO discharge', resp) |
3722 | + _print_error_message("get SSO discharge", resp) |
3723 | resp.raise_for_status() |
3724 | - return resp.json()['discharge_macaroon'] |
3725 | + return resp.json()["discharge_macaroon"] |
3726 | |
3727 | |
3728 | def refresh_sso_discharge(store, unbound_discharge_raw): |
3729 | - refresh_url = urllib.parse.urljoin( |
3730 | - store.get('sso_url'), '/api/v2/tokens/refresh') |
3731 | - data = {'discharge_macaroon': unbound_discharge_raw} |
3732 | - resp = requests.post( |
3733 | - refresh_url, headers={'Accept': 'application/json'}, json=data) |
3734 | + refresh_url = urllib.parse.urljoin(store.get("sso_url"), "/api/v2/tokens/refresh") |
3735 | + data = {"discharge_macaroon": unbound_discharge_raw} |
3736 | + resp = requests.post(refresh_url, headers={"Accept": "application/json"}, json=data) |
3737 | if not resp.ok: |
3738 | - _print_error_message('refresh SSO discharge', resp) |
3739 | + _print_error_message("refresh SSO discharge", resp) |
3740 | resp.raise_for_status() |
3741 | - return resp.json()['discharge_macaroon'] |
3742 | + return resp.json()["discharge_macaroon"] |
3743 | |
3744 | |
3745 | def _deserialize_macaroon(name, value): |
3746 | if value is None: |
3747 | - raise exceptions.InvalidCredentials('no {} macaroon'.format(name)) |
3748 | + raise exceptions.InvalidCredentials("no {} macaroon".format(name)) |
3749 | try: |
3750 | return Macaroon.deserialize(value) |
3751 | except Exception: |
3752 | raise exceptions.InvalidCredentials( |
3753 | - 'failed to deserialize {} macaroon'.format(name)) |
3754 | + "failed to deserialize {} macaroon".format(name) |
3755 | + ) |
3756 | |
3757 | |
3758 | def _get_macaroon_auth(store): |
3759 | """Return an Authorization header containing store macaroons.""" |
3760 | - root_raw = store.get('root') |
3761 | - root = _deserialize_macaroon('root', root_raw) |
3762 | - unbound_discharge_raw = store.get('unbound_discharge') |
3763 | + root_raw = store.get("root") |
3764 | + root = _deserialize_macaroon("root", root_raw) |
3765 | + unbound_discharge_raw = store.get("unbound_discharge") |
3766 | unbound_discharge = _deserialize_macaroon( |
3767 | - 'unbound discharge', unbound_discharge_raw) |
3768 | + "unbound discharge", unbound_discharge_raw |
3769 | + ) |
3770 | bound_discharge = root.prepare_for_request(unbound_discharge) |
3771 | bound_discharge_raw = bound_discharge.serialize() |
3772 | - return 'Macaroon root="{}", discharge="{}"'.format( |
3773 | - root_raw, bound_discharge_raw) |
3774 | + return 'Macaroon root="{}", discharge="{}"'.format(root_raw, bound_discharge_raw) |
3775 | |
3776 | |
3777 | def _get_basic_auth(password): |
3778 | """Build the basic auth for interacting with an offline proxy""" |
3779 | # XXX twom 2019-03-15 Hardcoded username, awaiting user management |
3780 | - username = 'admin' |
3781 | - credentials = '{}:{}'.format(username, password) |
3782 | + username = "admin" |
3783 | + credentials = "{}:{}".format(username, password) |
3784 | try: |
3785 | - encoded_credentials = base64.b64encode(credentials.encode('UTF-8')) |
3786 | + encoded_credentials = base64.b64encode(credentials.encode("UTF-8")) |
3787 | except UnicodeEncodeError: |
3788 | - logger.error('Unable to encode password to UTF-8') |
3789 | + logger.error("Unable to encode password to UTF-8") |
3790 | raise |
3791 | - return 'Basic {}'.format(encoded_credentials.decode()) |
3792 | + return "Basic {}".format(encoded_credentials.decode()) |
3793 | |
3794 | |
3795 | def _raise_needs_refresh(response): |
3796 | - if (response.status_code == 401 and |
3797 | - response.headers.get('WWW-Authenticate') == ( |
3798 | - 'Macaroon needs_refresh=1')): |
3799 | + if response.status_code == 401 and response.headers.get("WWW-Authenticate") == ( |
3800 | + "Macaroon needs_refresh=1" |
3801 | + ): |
3802 | raise exceptions.StoreMacaroonNeedsRefresh() |
3803 | |
3804 | |
3805 | @@ -123,44 +121,45 @@ def refresh_if_necessary(store, func, *args, **kwargs): |
3806 | return func(*args, **kwargs) |
3807 | except exceptions.StoreMacaroonNeedsRefresh: |
3808 | unbound_discharge = refresh_sso_discharge( |
3809 | - store.get('sso_url'), store.get('unbound_discharge')) |
3810 | - store.set('unbound_discharge', unbound_discharge) |
3811 | + store.get("sso_url"), store.get("unbound_discharge") |
3812 | + ) |
3813 | + store.set("unbound_discharge", unbound_discharge) |
3814 | store.save() |
3815 | return func(*args, **kwargs) |
3816 | |
3817 | |
3818 | -def get_overrides(store, snap_name, series='16', password=None): |
3819 | +def get_overrides(store, snap_name, series="16", password=None): |
3820 | """Get all overrides for a snap.""" |
3821 | overrides_url = urllib.parse.urljoin( |
3822 | - store.get('gw_url'), |
3823 | - '/v2/metadata/overrides/{}'.format(urllib.parse.quote_plus(snap_name))) |
3824 | + store.get("gw_url"), |
3825 | + "/v2/metadata/overrides/{}".format(urllib.parse.quote_plus(snap_name)), |
3826 | + ) |
3827 | headers = { |
3828 | - 'X-Ubuntu-Series': series, |
3829 | + "X-Ubuntu-Series": series, |
3830 | } |
3831 | if password: |
3832 | - headers['Authorization'] = _get_basic_auth(password) |
3833 | + headers["Authorization"] = _get_basic_auth(password) |
3834 | else: |
3835 | - headers['Authorization'] = _get_macaroon_auth(store) |
3836 | + headers["Authorization"] = _get_macaroon_auth(store) |
3837 | resp = requests.get(overrides_url, headers=headers) |
3838 | _raise_needs_refresh(resp) |
3839 | if resp.status_code != 200: |
3840 | - _print_error_message('get overrides', resp) |
3841 | + _print_error_message("get overrides", resp) |
3842 | resp.raise_for_status() |
3843 | return resp.json() |
3844 | |
3845 | |
3846 | def set_overrides(store, overrides, password=None): |
3847 | """Add or remove channel map overrides for a snap.""" |
3848 | - overrides_url = urllib.parse.urljoin( |
3849 | - store.get('gw_url'), '/v2/metadata/overrides') |
3850 | + overrides_url = urllib.parse.urljoin(store.get("gw_url"), "/v2/metadata/overrides") |
3851 | if password: |
3852 | - headers = {'Authorization': _get_basic_auth(password)} |
3853 | + headers = {"Authorization": _get_basic_auth(password)} |
3854 | else: |
3855 | - headers = {'Authorization': _get_macaroon_auth(store)} |
3856 | + headers = {"Authorization": _get_macaroon_auth(store)} |
3857 | resp = requests.post(overrides_url, headers=headers, json=overrides) |
3858 | _raise_needs_refresh(resp) |
3859 | if resp.status_code != 200: |
3860 | - _print_error_message('set override', resp) |
3861 | + _print_error_message("set override", resp) |
3862 | resp.raise_for_status() |
3863 | return resp.json() |
3864 | |
3865 | @@ -176,10 +175,11 @@ def _print_error_message(action, response): |
3866 | try: |
3867 | json_document = response.json() |
3868 | error_list = json_document.get( |
3869 | - 'error-list', json_document.get('error_list', [])) |
3870 | + "error-list", json_document.get("error_list", []) |
3871 | + ) |
3872 | for error in error_list: |
3873 | - logger.error(error['message']) |
3874 | + logger.error(error["message"]) |
3875 | except json.JSONDecodeError: |
3876 | - logger.error('=' * 20) |
3877 | - logger.error(response.content.decode('UTF-8', errors='replace')) |
3878 | - logger.error('=' * 20) |
3879 | + logger.error("=" * 20) |
3880 | + logger.error(response.content.decode("UTF-8", errors="replace")) |
3881 | + logger.error("=" * 20) |
As we discussed in a call, I think we should first create a design doc for this essentially new tool -- snap name changed here and we have the opportunity to design a new command tree layout for example.
In the design I think we should agree on:
* The split between commands for managing online proxy and offline store.
* Command names/options.
* Snap name itself.
* The root command name.
* How to make it authority delegation future proof.
* The login command currently handles login to the online proxy only for the purposes of managing overrides. The login action itself also needs internet access. Should it be only available under the "online" subcommand tree.