Merge ~woutervb/snapstore-client:SN-399_use_click into snapstore-client:master

Proposed by Wouter van Bommel
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)
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

To post a comment you must log in.
Revision history for this message
Przemysław Suliga (suligap) wrote :

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.

Revision history for this message
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 change

Fixes SN-399

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.flake8 b/.flake8
2new file mode 100644
3index 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
11diff --git a/.gitignore b/.gitignore
12index 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/
23diff --git a/Makefile b/Makefile
24index 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)
66diff --git a/requirements-dev.txt b/requirements-dev.txt
67index 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
75diff --git a/requirements.txt b/requirements.txt
76index 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
86diff --git a/setup.py b/setup.py
87new file mode 100644
88index 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+)
109diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
110index 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
160diff --git a/snapstore b/snapstore
161deleted file mode 100755
162index 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())
299diff --git a/snapstore_client/cli.py b/snapstore_client/cli.py
300index 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+ )
509diff --git a/snapstore_client/config.py b/snapstore_client/config.py
510index 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
564diff --git a/snapstore_client/exceptions.py b/snapstore_client/exceptions.py
565index 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."
626diff --git a/snapstore_client/logic/login.py b/snapstore_client/logic/login.py
627index 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
735diff --git a/snapstore_client/logic/overrides.py b/snapstore_client/logic/overrides.py
736index 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))
880diff --git a/snapstore_client/logic/push.py b/snapstore_client/logic/push.py
881index 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
1274diff --git a/snapstore_client/logic/tests/test_login.py b/snapstore_client/logic/tests/test_login.py
1275index 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+ )
1807diff --git a/snapstore_client/logic/tests/test_overrides.py b/snapstore_client/logic/tests/test_overrides.py
1808index 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+ )
2408diff --git a/snapstore_client/logic/tests/test_push.py b/snapstore_client/logic/tests/test_push.py
2409index 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+ )
2695diff --git a/snapstore_client/presentation_helpers.py b/snapstore_client/presentation_helpers.py
2696index 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)
2730diff --git a/snapstore_client/tests/factory.py b/snapstore_client/tests/factory.py
2731index 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
2790diff --git a/snapstore_client/tests/matchers.py b/snapstore_client/tests/matchers.py
2791index 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+ )
2831diff --git a/snapstore_client/tests/test_cli.py b/snapstore_client/tests/test_cli.py
2832deleted file mode 100644
2833index 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())
2902diff --git a/snapstore_client/tests/test_config.py b/snapstore_client/tests/test_config.py
2903index 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__)
2953diff --git a/snapstore_client/tests/test_presentation_helpers.py b/snapstore_client/tests/test_presentation_helpers.py
2954index 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):
3150diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py
3151index 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+ )
3540diff --git a/snapstore_client/tests/testfixtures.py b/snapstore_client/tests/testfixtures.py
3541index 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+ )
3622diff --git a/snapstore_client/utils.py b/snapstore_client/utils.py
3623index 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
3659diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py
3660index 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)

Subscribers

People subscribed via source and target branches

to all changes: