Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/store-rest-helpers into lp:snappy-ecosystem-tests

Proposed by Heber Parrucci
Status: Merged
Approved by: Omer Akram
Approved revision: 20
Merged at revision: 21
Proposed branch: lp:~canonical-platform-qa/snappy-ecosystem-tests/store-rest-helpers
Merge into: lp:snappy-ecosystem-tests
Prerequisite: lp:~canonical-platform-qa/snappy-ecosystem-tests/adding-test-runner
Diff against target: 2361 lines (+2112/-101)
19 files modified
README.rst (+19/-0)
pylint.cfg (+1/-1)
requirements.txt (+3/-0)
snappy_ecosystem_tests/helpers/fixture_setup.py (+5/-6)
snappy_ecosystem_tests/helpers/snapcraft/__init__.py (+19/-0)
snappy_ecosystem_tests/helpers/snapcraft/client.py (+74/-0)
snappy_ecosystem_tests/helpers/snapcraft/config.py (+114/-0)
snappy_ecosystem_tests/helpers/snapcraft/constants.py (+52/-0)
snappy_ecosystem_tests/helpers/snapcraft/deprecations.py (+56/-0)
snappy_ecosystem_tests/helpers/snapcraft/indicators.py (+118/-0)
snappy_ecosystem_tests/helpers/snapcraft/options.py (+244/-0)
snappy_ecosystem_tests/helpers/store_apis/__init__.py (+19/-0)
snappy_ecosystem_tests/helpers/store_apis/errors.py (+371/-0)
snappy_ecosystem_tests/helpers/store_apis/rest_apis.py (+880/-0)
snappy_ecosystem_tests/helpers/store_apis/upload.py (+97/-0)
snappy_ecosystem_tests/snapcraft/__init__.py (+0/-19)
snappy_ecosystem_tests/snapcraft/snapcraft.py (+0/-74)
snappy_ecosystem_tests/tests/test_store_apis_login.py (+39/-0)
snappy_ecosystem_tests/tests/test_store_login.py (+1/-1)
To merge this branch: bzr merge lp:~canonical-platform-qa/snappy-ecosystem-tests/store-rest-helpers
Reviewer Review Type Date Requested Status
platform-qa-bot continuous-integration Approve
Omer Akram (community) Approve
Santiago Baldassin (community) Approve
Review via email: mp+317220@code.launchpad.net

This proposal supersedes a proposal from 2017-02-03.

Commit message

Adding helpers for Store RESTful APIs.
Also includes a first version of ecosystem test base clase and fixture setup.

Description of the change

This change adds helpers for Store RESTful APIs, a first version of snappy ecosystem test base class and a fixture setup

A simple test was added for making sure we can authenticate against Store RESTful APIs. You can run it: ./run_system_tests snappy_ecosystem_tests/tests/test_store_apis_login.py

To post a comment you must log in.
Revision history for this message
I Ahmad (iahmad) wrote : Posted in a previous version of this proposal

How about using it as it is from snapcraft/storeapi as it is, instead of making a copy? that way we won't have to catchup the changes upstream.

Also if we have an example test case along with the merge then it will become convenient to test the base and other utility classes.

Otherwise LGTM

Revision history for this message
Omer Akram (om26er) wrote : Posted in a previous version of this proposal

Thanks for working on this, I was not able to test the code but wrote some inline comments and suggestions.

review: Needs Fixing
Revision history for this message
Omer Akram (om26er) wrote : Posted in a previous version of this proposal

We are missing docstrings, so it would also be helpful if you could add docstrings for the methods.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Santiago Baldassin (sbaldassin) wrote : Posted in a previous version of this proposal

Looks good in general. I just think that we should rethink the way the different clients are represented here. So far it is a little bit confussing

review: Needs Fixing
Revision history for this message
Heber Parrucci (heber013) wrote : Posted in a previous version of this proposal

Reply inline. I will address other comments once we agree in this one, because it would make other comments as not valid depending on which approach we take: use snapcraft upstream code or not.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
7. By Heber Parrucci

Fixing comments on code review.
Fixing pylint issues.
Addind a simple test for login to Store REST API

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
8. By Heber Parrucci

fixing error in requirements.txt

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
9. By Heber Parrucci

removing duplicated dependencies in requirements.txt

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
10. By Heber Parrucci

changing dict initialization sintax according to PEP 448

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
11. By Heber Parrucci

Merge from parent branch and fixing pylint to met max-line-length=80

12. By Heber Parrucci

deleting file not needed

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
13. By Heber Parrucci

changing dict initialization

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
14. By Heber Parrucci

chaning dict initialization until jenkins slave is updated

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Omer Akram (om26er) wrote :

Can you tell how should the config file look like ? I ran the tests and got http://paste.ubuntu.com/24006501/ as the config file was probably empty.

review: Needs Information
15. By Heber Parrucci

Updating README.rst with user credentials instructions

Revision history for this message
Heber Parrucci (heber013) wrote :

> Can you tell how should the config file look like ? I ran the tests and got
> http://paste.ubuntu.com/24006501/ as the config file was probably empty.

Thanks for reviewing!
I have added a section in README that is called: 'User Credentials' here it explains the alternatives for providing them.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
16. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
17. By Heber Parrucci

Updating README.rst to make storing credentials section more clear.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
18. By Heber Parrucci

renaming class Store and adding a better docstring

19. By Heber Parrucci

fixing pylint

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Code looks good to me

review: Approve
20. By Heber Parrucci

updating license year in headers

Revision history for this message
Omer Akram (om26er) wrote :

code looks good. test passes. Lets land it.

review: Approve
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
21. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.rst'
2--- README.rst 2017-02-13 16:00:04 +0000
3+++ README.rst 2017-02-20 17:48:06 +0000
4@@ -56,6 +56,25 @@
5
6 pytest.cfg stores the config related to pytest
7
8+User Credentials
9+================
10+For storing user credentials you have 2 options:
11+
12+option 1 - You can create a config file in your local machine with path:
13+~/.config/ecosystem_tests.cfg
14+You can change the dir location of that config file by setting the env variable:
15+XDG_USER_CONFIG_HOME=^DIR_PATH^
16+
17+The config file should look like:
18+[user]
19+user_email=^USER_NAME^
20+user_password=^USER_PASSWORD^
21+
22+option 2 - Set the following environment variables:
23+user_email=^USER_NAME^
24+user_password=^USER_PASSWORD^
25+
26+
27 Changing store:
28 ===============
29 You can change the store to use in config file ecosystem_tests.cfg
30
31=== modified file 'pylint.cfg'
32--- pylint.cfg 2017-02-15 19:14:19 +0000
33+++ pylint.cfg 2017-02-20 17:48:06 +0000
34@@ -336,7 +336,7 @@
35 [DESIGN]
36
37 # Maximum number of arguments for function / method
38-max-args=5
39+max-args=8
40
41 # Argument names that match this expression will be ignored. Default to name
42 # with leading underscore
43
44=== modified file 'requirements.txt'
45--- requirements.txt 2017-02-13 17:43:02 +0000
46+++ requirements.txt 2017-02-20 17:48:06 +0000
47@@ -1,3 +1,4 @@
48+libnacl
49 pytest
50 testtools
51 unittest2
52@@ -11,4 +12,6 @@
53 pyxdg==0.25
54 jsonschema==2.5.1
55 pexpect
56+pymacaroons==0.9.2
57+requests-toolbelt==0.6.0
58 chromedriver_installer
59
60=== modified file 'snappy_ecosystem_tests/helpers/fixture_setup.py'
61--- snappy_ecosystem_tests/helpers/fixture_setup.py 2017-02-15 19:14:19 +0000
62+++ snappy_ecosystem_tests/helpers/fixture_setup.py 2017-02-20 17:48:06 +0000
63@@ -18,10 +18,13 @@
64 # along with this program. If not, see <http://www.gnu.org/licenses/>.
65 #
66
67-"""Manage general tests fixtures"""
68+
69+"""General fixtures related to the store and the tests environment"""
70
71 import os
72+
73 import fixtures
74+
75 from snappy_ecosystem_tests.commons.config import CONFIG_STACK
76 from snappy_ecosystem_tests.utils.storeconfig import get_current_store
77
78@@ -30,10 +33,8 @@
79 """Create a temporary directory an cd into it for the test duration."""
80
81 def setUp(self):
82- """Create a temporary directory an cd into it for the test duration."""
83 super().setUp()
84- current_dir = os.getcwd()
85- self.addCleanup(os.chdir, current_dir)
86+ self.addCleanup(os.chdir, os.getcwd())
87 os.chdir(self.path)
88
89
90@@ -43,10 +44,8 @@
91
92 def setUp(self):
93 super().setUp()
94-
95 current_environment = os.environ.copy()
96 os.environ = {}
97-
98 self.addCleanup(os.environ.update, current_environment)
99
100
101
102=== added directory 'snappy_ecosystem_tests/helpers/snapcraft'
103=== added file 'snappy_ecosystem_tests/helpers/snapcraft/__init__.py'
104--- snappy_ecosystem_tests/helpers/snapcraft/__init__.py 1970-01-01 00:00:00 +0000
105+++ snappy_ecosystem_tests/helpers/snapcraft/__init__.py 2017-02-20 17:48:06 +0000
106@@ -0,0 +1,19 @@
107+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
108+
109+#
110+# Snappy Ecosystem Tests
111+# Copyright (C) 2017 Canonical
112+#
113+# This program is free software: you can redistribute it and/or modify
114+# it under the terms of the GNU General Public License as published by
115+# the Free Software Foundation, either version 3 of the License, or
116+# (at your option) any later version.
117+#
118+# This program is distributed in the hope that it will be useful,
119+# but WITHOUT ANY WARRANTY; without even the implied warranty of
120+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
121+# GNU General Public License for more details.
122+#
123+# You should have received a copy of the GNU General Public License
124+# along with this program. If not, see <http://www.gnu.org/licenses/>.
125+#
126
127=== added file 'snappy_ecosystem_tests/helpers/snapcraft/client.py'
128--- snappy_ecosystem_tests/helpers/snapcraft/client.py 1970-01-01 00:00:00 +0000
129+++ snappy_ecosystem_tests/helpers/snapcraft/client.py 2017-02-20 17:48:06 +0000
130@@ -0,0 +1,74 @@
131+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
132+
133+#
134+# Snappy Ecosystem Tests
135+# Copyright (C) 2017 Canonical
136+#
137+# This program is free software: you can redistribute it and/or modify
138+# it under the terms of the GNU General Public License as published by
139+# the Free Software Foundation, either version 3 of the License, or
140+# (at your option) any later version.
141+#
142+# This program is distributed in the hope that it will be useful,
143+# but WITHOUT ANY WARRANTY; without even the implied warranty of
144+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
145+# GNU General Public License for more details.
146+#
147+# You should have received a copy of the GNU General Public License
148+# along with this program. If not, see <http://www.gnu.org/licenses/>.
149+#
150+
151+"""Snapcraft client helpers"""
152+
153+import pexpect
154+
155+
156+# login credentials exported by shell environment
157+from snappy_ecosystem_tests.utils import storeconfig
158+
159+LOGIN_EMAIL, LOGIN_PASSWORD = storeconfig.get_store_credentials()
160+
161+
162+class Snapcraft(object):
163+ """Contain Snapcraft specific functionality to use via command
164+ line interface"""
165+ def __init__(self):
166+ """Create new snapcraft instance."""
167+ self._login = False
168+ self._cleanup()
169+
170+ def _cleanup(self):
171+ """Perform cleanup actions"""
172+ self.logout()
173+
174+ def logout(self):
175+ """logout of snapcraft store session"""
176+ child = pexpect.spawn("snapcraft logout")
177+ err = child.expect('Credentials cleared.')
178+ child.terminate(True)
179+ child.wait()
180+ if err is not 0:
181+ raise ValueError("Failed to logout")
182+
183+ self._login = False
184+
185+ def login(self):
186+ """login to store using the credential environment variables"""
187+ child = pexpect.spawn("snapcraft login")
188+ child.expect('Email: ')
189+ child.sendline(LOGIN_EMAIL)
190+ child.expect('Password:')
191+ child.sendline(LOGIN_PASSWORD)
192+ err = child.expect('Login successful')
193+ if err is not 0:
194+ raise ValueError("Failed to login")
195+
196+ self._login = True
197+
198+ def list_registered(self):
199+ """call snapcraft list-registered command,
200+ raise exception if not logged in"""
201+ if self._login is False:
202+ raise ValueError("User is not logged in, "
203+ "please login before using this command")
204+ return pexpect.spawnu("snapcraft list-registered").read()
205
206=== added file 'snappy_ecosystem_tests/helpers/snapcraft/config.py'
207--- snappy_ecosystem_tests/helpers/snapcraft/config.py 1970-01-01 00:00:00 +0000
208+++ snappy_ecosystem_tests/helpers/snapcraft/config.py 2017-02-20 17:48:06 +0000
209@@ -0,0 +1,114 @@
210+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
211+
212+#
213+# Snappy Ecosystem Tests
214+# Copyright (C) 2017 Canonical
215+#
216+# This program is free software: you can redistribute it and/or modify
217+# it under the terms of the GNU General Public License as published by
218+# the Free Software Foundation, either version 3 of the License, or
219+# (at your option) any later version.
220+#
221+# This program is distributed in the hope that it will be useful,
222+# but WITHOUT ANY WARRANTY; without even the implied warranty of
223+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
224+# GNU General Public License for more details.
225+#
226+# You should have received a copy of the GNU General Public License
227+# along with this program. If not, see <http://www.gnu.org/licenses/>.
228+#
229+
230+"""Helpers to store/manage/load snapcraft configuration in a config file"""
231+
232+import configparser
233+import logging
234+import os
235+import urllib.parse
236+
237+from snappy_ecosystem_tests.commons.config import CONFIG_STACK
238+from xdg import BaseDirectory
239+
240+LOCAL_CONFIG_FILENAME = '.snapcraft/snapcraft.cfg'
241+
242+LOGGER = logging.getLogger(__name__)
243+
244+
245+class SnapcraftConfig(object):
246+ """Hold configuration options in sections.
247+ There can be two sections for the sso related credentials: production and
248+ staging. This is governed by the UBUNTU_SSO_API_ROOT_URL environment
249+ variable. Other sections are ignored but preserved.
250+ """
251+
252+ def __init__(self):
253+ self.parser = configparser.ConfigParser()
254+ self.filename = None
255+ self.load()
256+
257+ @staticmethod
258+ def _section_name():
259+ """Return a section"""
260+ # The only section we care about is the host from the SSO url
261+ url = os.environ.get('UBUNTU_SSO_API_ROOT_URL',
262+ CONFIG_STACK.config.get('production_urls', 'sso'))
263+ return urllib.parse.urlparse(url).netloc
264+
265+ def get(self, option_name):
266+ """Get an option name for a given section"""
267+ try:
268+ return self.parser.get(self._section_name(), option_name)
269+ except (configparser.NoSectionError,
270+ configparser.NoOptionError,
271+ KeyError):
272+ return None
273+
274+ def set(self, option_name, value):
275+ """Set an option with the given value"""
276+ section_name = self._section_name()
277+ if not self.parser.has_section(section_name):
278+ self.parser.add_section(section_name)
279+ return self.parser.set(section_name, option_name, value)
280+
281+ def is_empty(self):
282+ """Return True if snapcraft config does not have the default section
283+ name"""
284+ # Only check the current section
285+ section_name = self._section_name()
286+ if self.parser.has_section(section_name):
287+ if self.parser.options(section_name):
288+ return False
289+ return True
290+
291+ def load(self):
292+ """Load the snapcraft configuration"""
293+ # Local configurations (per project) are supposed to be static.
294+ # That's why it's only checked for 'loading' and never written to.
295+ # Essentially, all authentication-related changes, like login/logout
296+ # or macaroon-refresh, will not be persisted for the next runs.
297+ if os.path.exists(LOCAL_CONFIG_FILENAME):
298+ self.parser.read(LOCAL_CONFIG_FILENAME)
299+ LOGGER.warning(
300+ 'Using local configuration (`%s`), changes will '
301+ 'not be persisted.', LOCAL_CONFIG_FILENAME)
302+ return
303+
304+ self.filename = BaseDirectory.load_first_config(
305+ 'snapcraft', 'snapcraft.cfg')
306+ if self.filename and os.path.exists(self.filename):
307+ self.parser.read(self.filename)
308+
309+ @staticmethod
310+ def save_path():
311+ """Return the save path for the snapcraft configuration"""
312+ return os.path.join(BaseDirectory.save_config_path('snapcraft'),
313+ 'snapcraft.cfg')
314+
315+ def save(self):
316+ """Save the current configuration in the save path"""
317+ self.filename = self.save_path()
318+ with open(self.filename, 'w') as _f:
319+ self.parser.write(_f)
320+
321+ def clear(self):
322+ """Clear the current configuration"""
323+ self.parser.remove_section(self._section_name())
324
325=== added file 'snappy_ecosystem_tests/helpers/snapcraft/constants.py'
326--- snappy_ecosystem_tests/helpers/snapcraft/constants.py 1970-01-01 00:00:00 +0000
327+++ snappy_ecosystem_tests/helpers/snapcraft/constants.py 2017-02-20 17:48:06 +0000
328@@ -0,0 +1,52 @@
329+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
330+
331+#
332+# Snappy Ecosystem Tests
333+# Copyright (C) 2017 Canonical
334+#
335+# This program is free software: you can redistribute it and/or modify
336+# it under the terms of the GNU General Public License as published by
337+# the Free Software Foundation, either version 3 of the License, or
338+# (at your option) any later version.
339+#
340+# This program is distributed in the hope that it will be useful,
341+# but WITHOUT ANY WARRANTY; without even the implied warranty of
342+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
343+# GNU General Public License for more details.
344+#
345+# You should have received a copy of the GNU General Public License
346+# along with this program. If not, see <http://www.gnu.org/licenses/>.
347+#
348+
349+"""Store snapcraft constants"""
350+
351+from __future__ import absolute_import, unicode_literals
352+
353+
354+DEFAULT_SERIES = '16'
355+SCAN_STATUS_POLL_DELAY = 5
356+SCAN_STATUS_POLL_RETRIES = 5
357+
358+# Messages and warnings.
359+MISSING_AGREEMENT = 'Developer has not signed agreement.'
360+MISSING_NAMESPACE = 'Developer profile is missing short namespace.'
361+AGREEMENT_ERROR = (
362+ 'You must agree to the developer terms and conditions to upload snaps.')
363+NAMESPACE_ERROR = (
364+ 'You need to set a username. It will appear in the developer field '
365+ 'alongside the other details for your snap. Please visit {} and login '
366+ 'again.')
367+AGREEMENT_INPUT_MSG = (
368+ 'Do you agree to the developer terms and conditions. ({})? [y/N] ')
369+AGREEMENT_SIGN_ERROR = (
370+ 'Unexpected error encountered during signing the developer terms and '
371+ 'conditions. Please visit {} and agree to the terms and conditions before '
372+ 'continuing.')
373+TWO_FACTOR_WARNING = (
374+ 'We strongly recommend enabling multi-factor authentication: '
375+ 'https://help.ubuntu.com/community/SSO/FAQs/2FA')
376+INVALID_CREDENTIALS = 'Invalid credentials supplied.'
377+AUTHENTICATION_ERROR = ('Problems encountered when authenticating your '
378+ 'credentials.')
379+ACCOUNT_INFORMATION_ERROR = ('Unexpected error when obtaining your account '
380+ 'information.')
381
382=== added file 'snappy_ecosystem_tests/helpers/snapcraft/deprecations.py'
383--- snappy_ecosystem_tests/helpers/snapcraft/deprecations.py 1970-01-01 00:00:00 +0000
384+++ snappy_ecosystem_tests/helpers/snapcraft/deprecations.py 2017-02-20 17:48:06 +0000
385@@ -0,0 +1,56 @@
386+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
387+
388+#
389+# Snappy Ecosystem Tests
390+# Copyright (C) 2017 Canonical
391+#
392+# This program is free software: you can redistribute it and/or modify
393+# it under the terms of the GNU General Public License as published by
394+# the Free Software Foundation, either version 3 of the License, or
395+# (at your option) any later version.
396+#
397+# This program is distributed in the hope that it will be useful,
398+# but WITHOUT ANY WARRANTY; without even the implied warranty of
399+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
400+# GNU General Public License for more details.
401+#
402+# You should have received a copy of the GNU General Public License
403+# along with this program. If not, see <http://www.gnu.org/licenses/>.
404+#
405+
406+"""Handle surfacing deprecation notices.
407+
408+When a new deprecation has occurred, write a Deprecation Notice for it on the
409+wiki, assigning it the next ID (DN1, DN2, etc.). Then add that ID along with a
410+brief message for surfacing to the user into the _DEPRECATION_MESSAGES in this
411+module.
412+"""
413+
414+import logging
415+
416+_DEPRECATION_MESSAGES = {
417+ 'dn1': "The 'snap' keyword has been replaced by 'prime'.",
418+ 'dn2': "Custom plugins should now be placed in 'snap/plugins'.",
419+ 'dn3': "Assets in 'setup/gui' should now be placed in 'snap/gui'.",
420+}
421+
422+_DEPRECATION_URL_FMT = 'http://snapcraft.io/docs/deprecation-notices/{id}'
423+
424+LOGGER = logging.getLogger(__name__)
425+
426+
427+def _deprecation_message(_id):
428+ """Return the deprecation message for the given id
429+ :raise RuntimeError
430+ """
431+ message = _DEPRECATION_MESSAGES.get(_id)
432+ if not message:
433+ raise RuntimeError('No deprecation notice with id {!r}'.format(id))
434+ return message
435+
436+
437+def handle_deprecation_notice(_id):
438+ """Handle deprecation notice by logging a message"""
439+ message = _deprecation_message(_id)
440+ LOGGER.warning('DEPRECATED: %s\nSee %s for more information.',
441+ message, _DEPRECATION_URL_FMT.format(id=_id))
442
443=== added file 'snappy_ecosystem_tests/helpers/snapcraft/indicators.py'
444--- snappy_ecosystem_tests/helpers/snapcraft/indicators.py 1970-01-01 00:00:00 +0000
445+++ snappy_ecosystem_tests/helpers/snapcraft/indicators.py 2017-02-20 17:48:06 +0000
446@@ -0,0 +1,118 @@
447+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
448+
449+#
450+# Snappy Ecosystem Tests
451+# Copyright (C) 2017 Canonical
452+#
453+# This program is free software: you can redistribute it and/or modify
454+# it under the terms of the GNU General Public License as published by
455+# the Free Software Foundation, either version 3 of the License, or
456+# (at your option) any later version.
457+#
458+# This program is distributed in the hope that it will be useful,
459+# but WITHOUT ANY WARRANTY; without even the implied warranty of
460+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
461+# GNU General Public License for more details.
462+#
463+# You should have received a copy of the GNU General Public License
464+# along with this program. If not, see <http://www.gnu.org/licenses/>.
465+#
466+
467+"""Helpers for snapcraft indicators with pretty progress bar"""
468+
469+import os
470+import sys
471+
472+from urllib.request import urlretrieve
473+from progressbar import (
474+ AnimatedMarker,
475+ Bar,
476+ Percentage,
477+ ProgressBar,
478+ UnknownLength,
479+)
480+
481+
482+def _init_progress_bar(total_length, destination, message=None):
483+ """Init the progress bar with the given length, destination and message"""
484+ if not message:
485+ message = 'Downloading {!r}'.format(os.path.basename(destination))
486+
487+ valid_length = total_length and total_length > 0
488+
489+ if valid_length and is_dumb_terminal():
490+ widgets = [message, ' ', Percentage()]
491+ maxval = total_length
492+ elif valid_length and not is_dumb_terminal():
493+ widgets = [message,
494+ Bar(marker='=', left='[', right=']'),
495+ ' ', Percentage()]
496+ maxval = total_length
497+ elif not valid_length and is_dumb_terminal():
498+ widgets = [message]
499+ maxval = UnknownLength
500+ else:
501+ widgets = [message, AnimatedMarker()]
502+ maxval = UnknownLength
503+
504+ return ProgressBar(widgets=widgets, maxval=maxval)
505+
506+
507+def download_requests_stream(request_stream, destination, message=None):
508+ """This is a facility to download a request with nice progress bars."""
509+
510+ # Doing len(request_stream.content) may defeat the purpose of a
511+ # progress bar
512+ total_length = 0
513+ if not request_stream.headers.get('Content-Encoding', ''):
514+ total_length = int(request_stream.headers.get('Content-Length', '0'))
515+
516+ total_read = 0
517+ progress_bar = _init_progress_bar(total_length, destination, message)
518+ progress_bar.start()
519+ with open(destination, 'wb') as destination_file:
520+ for buf in request_stream.iter_content(1024):
521+ destination_file.write(buf)
522+ total_read += len(buf)
523+ progress_bar.update(total_read)
524+ progress_bar.finish()
525+
526+
527+class UrllibDownloader(object):
528+ """This is a facility to download an uri with nice progress bars."""
529+
530+ def __init__(self, uri, destination, message=None):
531+ self.uri = uri
532+ self.destination = destination
533+ self.message = message
534+ self.progress_bar = None
535+
536+ def download(self):
537+ """Download with a pretty progress bar"""
538+ urlretrieve(self.uri, self.destination, self._progress_callback)
539+
540+ if self.progress_bar:
541+ self.progress_bar.finish()
542+
543+ def _progress_callback(self, block_num, block_size, total_length):
544+ """Callback to be used when downloading"""
545+ if not self.progress_bar:
546+ self.progress_bar = _init_progress_bar(
547+ total_length, self.destination, self.message)
548+ self.progress_bar.start()
549+
550+ total_read = block_num * block_size
551+ self.progress_bar.update(
552+ min(total_read, total_length) if total_length > 0 else total_read)
553+
554+
555+def download_urllib_source(uri, destination, message=None):
556+ """Perform a download from the given uri to the given destination"""
557+ UrllibDownloader(uri, destination, message).download()
558+
559+
560+def is_dumb_terminal():
561+ """Return True if on a dumb terminal."""
562+ is_stdout_tty = os.isatty(sys.stdout.fileno())
563+ is_term_dumb = os.environ.get('TERM', '') == 'dumb'
564+ return not is_stdout_tty or is_term_dumb
565
566=== added file 'snappy_ecosystem_tests/helpers/snapcraft/options.py'
567--- snappy_ecosystem_tests/helpers/snapcraft/options.py 1970-01-01 00:00:00 +0000
568+++ snappy_ecosystem_tests/helpers/snapcraft/options.py 2017-02-20 17:48:06 +0000
569@@ -0,0 +1,244 @@
570+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
571+
572+#
573+# Snappy Ecosystem Tests
574+# Copyright (C) 2017 Canonical
575+#
576+# This program is free software: you can redistribute it and/or modify
577+# it under the terms of the GNU General Public License as published by
578+# the Free Software Foundation, either version 3 of the License, or
579+# (at your option) any later version.
580+#
581+# This program is distributed in the hope that it will be useful,
582+# but WITHOUT ANY WARRANTY; without even the implied warranty of
583+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
584+# GNU General Public License for more details.
585+#
586+# You should have received a copy of the GNU General Public License
587+# along with this program. If not, see <http://www.gnu.org/licenses/>.
588+#
589+
590+"""Manage snapcraft project options"""
591+
592+import logging
593+import multiprocessing
594+import os
595+import platform
596+
597+from snappy_ecosystem_tests.helpers.snapcraft.deprecations import (
598+ handle_deprecation_notice
599+)
600+
601+LOGGER = logging.getLogger(__name__)
602+
603+
604+_ARCH_TRANSLATIONS = {
605+ 'armv7l': {
606+ 'kernel': 'arm',
607+ 'deb': 'armhf',
608+ 'cross-compiler-prefix': 'arm-linux-gnueabihf-',
609+ 'cross-build-packages': ['gcc-arm-linux-gnueabihf'],
610+ 'triplet': 'arm-linux-gnueabihf',
611+ 'core-dynamic-linker': 'lib/ld-linux-armhf.so.3',
612+ },
613+ 'aarch64': {
614+ 'kernel': 'arm64',
615+ 'deb': 'arm64',
616+ 'cross-compiler-prefix': 'aarch64-linux-gnu-',
617+ 'cross-build-packages': ['gcc-aarch64-linux-gnu'],
618+ 'triplet': 'aarch64-linux-gnu',
619+ 'core-dynamic-linker': 'lib/ld-linux-aarch64.so.1',
620+ },
621+ 'i686': {
622+ 'kernel': 'x86',
623+ 'deb': 'i386',
624+ 'triplet': 'i386-linux-gnu',
625+ },
626+ 'ppc64le': {
627+ 'kernel': 'powerpc',
628+ 'deb': 'ppc64el',
629+ 'cross-compiler-prefix': 'powerpc64le-linux-gnu-',
630+ 'cross-build-packages': ['gcc-powerpc64le-linux-gnu'],
631+ 'triplet': 'powerpc64le-linux-gnu',
632+ 'core-dynamic-linker': '/lib64/ld64.so.2',
633+ },
634+ 'ppc': {
635+ 'kernel': 'powerpc',
636+ 'deb': 'powerpc',
637+ 'cross-compiler-prefix': 'powerpc-linux-gnu-',
638+ 'cross-build-packages': ['gcc-powerpc-linux-gnu'],
639+ 'triplet': 'powerpc-linux-gnu',
640+ },
641+ 'x86_64': {
642+ 'kernel': 'x86',
643+ 'deb': 'amd64',
644+ 'triplet': 'x86_64-linux-gnu',
645+ 'core-dynamic-linker': 'lib64/ld-linux-x86-64.so.2',
646+ },
647+ 's390x': {
648+ 'kernel': 's390x',
649+ 'deb': 's390x',
650+ 'cross-compiler-prefix': 's390x-linux-gnu-',
651+ 'cross-build-packages': ['gcc-s390x-linux-gnu'],
652+ 'triplet': 's390x-linux-gnu',
653+ 'core-dynamic-linker': '/lib/ld64.so.1',
654+ }
655+}
656+
657+
658+_32BIT_USERSPACE_ARCHITECTURE = {
659+ 'aarch64': 'armv7l',
660+ 'armv8l': 'armv7l',
661+ 'ppc64le': 'ppc',
662+ 'x86_64': 'i686',
663+}
664+
665+
666+def _get_platform_architecture():
667+ """Return the current platform architecture of the machine"""
668+ architecture = platform.machine()
669+ if platform.architecture()[0] == '32bit':
670+ userspace = _32BIT_USERSPACE_ARCHITECTURE.get(architecture)
671+ if userspace:
672+ architecture = userspace
673+ return architecture
674+
675+
676+class ProjectOptions:
677+ """Represents a snapcraft project options"""
678+ def __init__(self, use_geoip=False, parallel_builds=True,
679+ target_deb_arch=None, debug=False):
680+ self.__project_dir = os.getcwd()
681+ self.__use_geoip = use_geoip
682+ self.__parallel_builds = parallel_builds
683+ self._set_machine(target_deb_arch)
684+ self.__debug = debug
685+
686+ @property
687+ def use_geoip(self):
688+ """Return True if the project uses geoip"""
689+ return self.__use_geoip
690+
691+ @property
692+ def parallel_builds(self):
693+ """Return True if the project uses parallel build"""
694+ return self.__parallel_builds
695+
696+ @property
697+ def parallel_build_count(self):
698+ """Return the amount of cpus used for parallel builds"""
699+ build_count = 1
700+ if self.__parallel_builds:
701+ try:
702+ build_count = multiprocessing.cpu_count()
703+ except NotImplementedError:
704+ LOGGER.warning(
705+ 'Unable to determine CPU count; disabling parallel builds')
706+ return build_count
707+
708+ @property
709+ def is_cross_compiling(self):
710+ """Return True if the target machine does not match
711+ the platform arch of the project"""
712+ return self.__target_machine != self.__platform_arch
713+
714+ @property
715+ def cross_compiler_prefix(self):
716+ """Return the cross compiler prefix"""
717+ try:
718+ return self.__machine_info['cross-compiler-prefix']
719+ except KeyError:
720+ raise EnvironmentError(
721+ 'Cross compilation not support for target arch {}'.format(
722+ self.__target_machine))
723+
724+ @property
725+ def additional_build_packages(self):
726+ """Return the additional build packages from the machine info"""
727+ packages = []
728+ if self.is_cross_compiling:
729+ packages.extend(self.__machine_info.get(
730+ 'cross-build-packages', []))
731+ return packages
732+
733+ @property
734+ def arch_triplet(self):
735+ """Return the arch triplet from the machine info"""
736+ return self.__machine_info['triplet']
737+
738+ @property
739+ def deb_arch(self):
740+ """Return the deb arch from the machine info"""
741+ return self.__machine_info['deb']
742+
743+ @property
744+ def kernel_arch(self):
745+ """Return the kernel arch from the machine info"""
746+ return self.__machine_info['kernel']
747+
748+ @property
749+ def local_plugins_dir(self):
750+ """Return the plugin directory"""
751+ deprecated_plugins_dir = os.path.join(self.parts_dir, 'plugins')
752+ if os.path.exists(deprecated_plugins_dir):
753+ handle_deprecation_notice('dn2')
754+ return deprecated_plugins_dir
755+ return os.path.join(self.__project_dir, 'snap', 'plugins')
756+
757+ @property
758+ def parts_dir(self):
759+ """Return the parts directory"""
760+ return os.path.join(self.__project_dir, 'parts')
761+
762+ @property
763+ def stage_dir(self):
764+ """Return the stage directory"""
765+ return os.path.join(self.__project_dir, 'stage')
766+
767+ @property
768+ def snap_dir(self):
769+ """Return the snap directory"""
770+ return os.path.join(self.__project_dir, 'prime')
771+
772+ @property
773+ def debug(self):
774+ """Return debug of the project options"""
775+ return self.__debug
776+
777+ def get_core_dynamic_linker(self):
778+ """Returns the dynamic linker used for the targetted core.
779+ If not found realpath for `/lib/ld-linux.so.2` is returned.
780+ However if core is not installed None will be returned.
781+ """
782+ core_path = os.path.join('/snap', 'core', 'current')
783+ core_dynamic_linker = self.__machine_info.get('core-dynamic-linker',
784+ 'lib/ld-linux.so.2')
785+
786+ try:
787+ dynamic_linker_resolved_path = os.readlink(
788+ os.path.join(core_path, core_dynamic_linker))
789+ dynamic_linker_path = os.path.join(
790+ core_path, dynamic_linker_resolved_path.lstrip('/'))
791+ except FileNotFoundError:
792+ dynamic_linker_path = None
793+
794+ return dynamic_linker_path
795+
796+ def _set_machine(self, target_deb_arch):
797+ """Set the target machine with the given deb arch"""
798+ self.__platform_arch = _get_platform_architecture()
799+ if not target_deb_arch:
800+ self.__target_machine = self.__platform_arch
801+ else:
802+ self.__target_machine = _find_machine(target_deb_arch)
803+ LOGGER.info('Setting target machine to {%s}', target_deb_arch)
804+ self.__machine_info = _ARCH_TRANSLATIONS[self.__target_machine]
805+
806+
807+def _find_machine(deb_arch):
808+ """Return a machine for the given deb arch"""
809+ for machine in _ARCH_TRANSLATIONS:
810+ if _ARCH_TRANSLATIONS[machine].get('deb', '') == deb_arch:
811+ return machine
812+ raise EnvironmentError(
813+ 'Cannot set machine from deb_arch {!r}'.format(deb_arch))
814
815=== renamed directory 'snappy_ecosystem_tests/snapd' => 'snappy_ecosystem_tests/helpers/snapd'
816=== added directory 'snappy_ecosystem_tests/helpers/store_apis'
817=== added file 'snappy_ecosystem_tests/helpers/store_apis/__init__.py'
818--- snappy_ecosystem_tests/helpers/store_apis/__init__.py 1970-01-01 00:00:00 +0000
819+++ snappy_ecosystem_tests/helpers/store_apis/__init__.py 2017-02-20 17:48:06 +0000
820@@ -0,0 +1,19 @@
821+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
822+
823+#
824+# Snappy Ecosystem Tests
825+# Copyright (C) 2017 Canonical
826+#
827+# This program is free software: you can redistribute it and/or modify
828+# it under the terms of the GNU General Public License as published by
829+# the Free Software Foundation, either version 3 of the License, or
830+# (at your option) any later version.
831+#
832+# This program is distributed in the hope that it will be useful,
833+# but WITHOUT ANY WARRANTY; without even the implied warranty of
834+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
835+# GNU General Public License for more details.
836+#
837+# You should have received a copy of the GNU General Public License
838+# along with this program. If not, see <http://www.gnu.org/licenses/>.
839+#
840
841=== added file 'snappy_ecosystem_tests/helpers/store_apis/errors.py'
842--- snappy_ecosystem_tests/helpers/store_apis/errors.py 1970-01-01 00:00:00 +0000
843+++ snappy_ecosystem_tests/helpers/store_apis/errors.py 2017-02-20 17:48:06 +0000
844@@ -0,0 +1,371 @@
845+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
846+#
847+# Copyright (C) 2017 Canonical Ltd
848+#
849+# This program is free software: you can redistribute it and/or modify
850+# it under the terms of the GNU General Public License version 3 as
851+# published by the Free Software Foundation.
852+#
853+# This program is distributed in the hope that it will be useful,
854+# but WITHOUT ANY WARRANTY; without even the implied warranty of
855+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
856+# GNU General Public License for more details.
857+#
858+# You should have received a copy of the GNU General Public License
859+# along with this program. If not, see <http://www.gnu.org/licenses/>.
860+
861+"""All the store exceptions"""
862+
863+from simplejson.scanner import JSONDecodeError
864+
865+
866+class StoreException(Exception):
867+ """Base class for all store exceptions.
868+ :cvar fmt: A format string that daughter classes override
869+ """
870+ fmt = 'Daughter classes should redefine this'
871+
872+ def __init__(self, **kwargs):
873+ super().__init__()
874+ for key, value in kwargs.items():
875+ setattr(self, key, value)
876+
877+ def __str__(self):
878+ return self.fmt.format([], **self.__dict__)
879+
880+
881+class InvalidCredentialsError(StoreException):
882+ """Exception to be raised when credentials are invalid"""
883+ fmt = 'Invalid credentials: {message}.'
884+
885+ def __init__(self, message):
886+ super().__init__(message=message)
887+
888+
889+class SnapNotFoundError(StoreException):
890+ """Exception to be raised when a snap is not found in the store"""
891+ __FMT_ARCH_CHANNEL = (
892+ 'Snap {name!r} for {arch!r} cannot be found in the {channel!r} '
893+ 'channel.')
894+ __FMT_CHANNEL = 'Snap {name!r} was not found in the {channel!r} channel.'
895+ __FMT_SERIES_ARCH = (
896+ 'Snap {name!r} for {arch!r} was not found in {series!r} series.')
897+ __FMT_SERIES = 'Snap {name!r} was not found in {series!r} series.'
898+
899+ fmt = 'Snap {name!r} was not found.'
900+
901+ def __init__(self, name, channel=None, arch=None, series=None):
902+ if channel and arch:
903+ self.fmt = self.__FMT_ARCH_CHANNEL
904+ elif channel:
905+ self.fmt = self.__FMT_CHANNEL
906+ elif series and arch:
907+ self.fmt = self.__FMT_SERIES_ARCH
908+ elif series:
909+ self.fmt = self.__FMT_SERIES
910+
911+ super().__init__(name=name, channel=channel, arch=arch, series=series)
912+
913+
914+class SHAMismatchError(StoreException):
915+ """Exception to be raised when there is a mismatch in sha512"""
916+ fmt = 'SHA512 checksum for {path} is not {expected_sha}.'
917+
918+ def __init__(self, path, expected_sha):
919+ super().__init__(path=path, expected_sha=expected_sha)
920+
921+
922+class StoreAuthenticationError(StoreException):
923+ """Exception to be raised when there is an authentication error in the
924+ store"""
925+ fmt = 'Authentication error: {}.'
926+
927+ def __init__(self, message):
928+ super().__init__(message=message)
929+
930+
931+class StoreTwoFactorAuthenticationRequired(StoreAuthenticationError):
932+ """Exception to be raised when the credentials were provided but 2-factor
933+ authentication was not and is required.
934+ """
935+ def __init__(self):
936+ super().__init__("Two-factor authentication required.")
937+
938+
939+class StoreMacaroonNeedsRefreshError(StoreException):
940+ """Exception to be raised when a request needs to refresh
941+ the macaroon to be authenticated properly"""
942+ fmt = 'Authentication macaroon needs to be refreshed.'
943+
944+
945+class DeveloperAgreementSignError(StoreException):
946+ """Exception to be raised when developer tries to do an operation
947+ in the store but he did not sign the Agreement yet"""
948+ fmt = (
949+ 'There was an error while signing developer agreement.\n'
950+ 'Reason: {reason!r}\n'
951+ 'Text: {text!r}')
952+
953+ def __init__(self, response):
954+ super().__init__(reason=response.reason, text=response.text)
955+
956+
957+class NeedTermsSignedError(StoreException):
958+ """Exception to be raised when an operation needs the Developer
959+ Terms of Service to be signed before proceed"""
960+ fmt = (
961+ 'Developer Terms of Service agreement must be signed '
962+ 'before continuing: {message}')
963+
964+ def __init__(self, message):
965+ super().__init__(message=message)
966+
967+
968+class StoreAccountInformationError(StoreException):
969+ """Exception to be raised when there was not possible
970+ to fetch the user account information"""
971+ fmt = 'Error fetching account information from store: {error}'
972+
973+ def __init__(self, response):
974+ error = '{} {}'.format(response.status_code, response.reason)
975+ extra = []
976+ try:
977+ response_json = response.json()
978+ if 'error_list' in response_json:
979+ error = ' '.join(
980+ error['message'] for error in response_json['error_list'])
981+ extra = [
982+ error['extra'] for error in response_json[
983+ 'error_list'] if 'extra' in error]
984+ except JSONDecodeError:
985+ pass
986+ super().__init__(error=error, extra=extra)
987+
988+
989+class StoreKeyRegistrationError(StoreException):
990+ """Exception to be raised when there was a problem
991+ registering a key in the store"""
992+ fmt = 'Key registration failed: {error}'
993+
994+ def __init__(self, response):
995+ error = '{} {}'.format(response.status_code, response.reason)
996+ try:
997+ response_json = response.json()
998+ if 'error_list' in response_json:
999+ error = ' '.join(
1000+ error['message'] for error in response_json['error_list'])
1001+ except JSONDecodeError:
1002+ pass
1003+ super().__init__(error=error)
1004+
1005+
1006+class StoreRegistrationError(StoreException):
1007+ """Exception to be raised when there was a problem registering
1008+ a snap name"""
1009+ __FMT_ALREADY_REGISTERED = (
1010+ 'The name {snap_name!r} is already taken.\n\n'
1011+ 'We can if needed rename snaps to ensure they match the expectations '
1012+ 'of most users. If you are the publisher most users expect for '
1013+ '{snap_name!r} then claim the name at {register_name_url!r}')
1014+
1015+ __FMT_ALREADY_OWNED = 'You already own the name {snap_name!r}.'
1016+
1017+ __FMT_RESERVED = (
1018+ 'The name {snap_name!r} is reserved.\n\n'
1019+ 'If you are the publisher most users expect for '
1020+ '{snap_name!r} then please claim the name at {register_name_url!r}')
1021+
1022+ __FMT_RETRY_WAIT = (
1023+ 'You must wait {retry_after} seconds before trying to register '
1024+ 'your next snap.')
1025+
1026+ fmt = 'Registration failed.'
1027+
1028+ __error_messages = {
1029+ 'already_registered': __FMT_ALREADY_REGISTERED,
1030+ 'already_owned': __FMT_ALREADY_OWNED,
1031+ 'reserved_name': __FMT_RESERVED,
1032+ 'register_window': __FMT_RETRY_WAIT,
1033+ }
1034+
1035+ def __init__(self, snap_name, response):
1036+ try:
1037+ response_json = response.json()
1038+ except JSONDecodeError:
1039+ response_json = {}
1040+
1041+ error_code = response_json.get('code')
1042+ if error_code:
1043+ # we default to self.fmt in case error_code is not mapped yet.
1044+ self.fmt = self.__error_messages.get(error_code, self.fmt)
1045+
1046+ super().__init__(snap_name=snap_name, **response_json)
1047+
1048+
1049+class StoreUploadError(StoreException):
1050+ """Exception to be raised when there was a problem
1051+ uploading a snap to the store"""
1052+ fmt = (
1053+ 'There was an error uploading the package.\n'
1054+ 'Reason: {reason!r}\n'
1055+ 'Text: {text!r}')
1056+
1057+ def __init__(self, response):
1058+ super().__init__(reason=response.reason, text=response.text)
1059+
1060+
1061+class StorePushError(StoreException):
1062+ """Exception to be raised when there was a problem
1063+ pushing a snap to the store"""
1064+ __FMT_NOT_REGISTERED = (
1065+ 'You are not the publisher or allowed to push revisions for this '
1066+ 'snap. To become the publisher, run `snapcraft register {snap_name}` '
1067+ 'and try to push again.')
1068+
1069+ fmt = 'Received {status_code!r}: {text!r}'
1070+
1071+ def __init__(self, snap_name, response):
1072+ try:
1073+ response_json = response.json()
1074+ except (AttributeError, JSONDecodeError):
1075+ response_json = {}
1076+
1077+ if response.status_code == 404:
1078+ self.fmt = self.__FMT_NOT_REGISTERED
1079+ elif response.status_code == 401 or response.status_code == 403:
1080+ try:
1081+ response_json['text'] = response.text
1082+ except AttributeError:
1083+ response_json['text'] = 'error while pushing'
1084+
1085+ super().__init__(snap_name=snap_name, status_code=response.status_code,
1086+ **response_json)
1087+
1088+
1089+class StoreReviewError(StoreException):
1090+ """Exception to be raised when there was a problem in the snap
1091+ that prevents it to be published/updated"""
1092+ __FMT_NEED_MANUAL_REVIEW = (
1093+ 'Publishing checks failed.\n'
1094+ 'To release this to stable channel please request a review on '
1095+ 'the snapcraft list.\n'
1096+ 'Use devmode in the edge or beta channels to disable confinement.')
1097+
1098+ __FMT_PROCESSING_ERROR = (
1099+ 'There has been a problem while analyzing the snap, check the snap '
1100+ 'and try to push again.')
1101+
1102+ __messages = {
1103+ 'need_manual_review': __FMT_NEED_MANUAL_REVIEW,
1104+ 'processing_error': __FMT_PROCESSING_ERROR,
1105+ }
1106+
1107+ def __init__(self, result):
1108+ self.fmt = self.__messages[result['code']]
1109+ super().__init__()
1110+
1111+
1112+class StoreReleaseError(StoreException):
1113+ """Exception to be raised when the snap could not be released"""
1114+ __FMT_NOT_REGISTERED = (
1115+ 'Sorry, try `snapcraft register {snap_name}` before trying to '
1116+ 'release or choose an existing revision.')
1117+
1118+ fmt = 'Received {status_code!r}: {text!r}'
1119+
1120+ def __init__(self, snap_name, response):
1121+ try:
1122+ response_json = response.json()
1123+ except (AttributeError, JSONDecodeError):
1124+ response_json = {}
1125+
1126+ if response.status_code == 404:
1127+ self.fmt = self.__FMT_NOT_REGISTERED
1128+ elif response.status_code == 401 or response.status_code == 403:
1129+ try:
1130+ response_json['text'] = response.text
1131+ except AttributeError:
1132+ response_json['text'] = 'error while releasing'
1133+ elif 'errors' in response_json:
1134+ self.fmt = '{errors}'
1135+
1136+ super().__init__(snap_name=snap_name, status_code=response.status_code,
1137+ **response_json)
1138+
1139+
1140+class StoreValidationError(StoreException):
1141+ """Exception to be raised when there was a validation
1142+ problem in the store"""
1143+ fmt = 'Received error {status_code!r}: {text!r}'
1144+
1145+ def __init__(self, response, message=None):
1146+ try:
1147+ response_json = response.json()
1148+ response_json['text'] = response.json()['error_list'][0]['message']
1149+ except (AttributeError, JSONDecodeError):
1150+ response_json = {'text': message or response}
1151+
1152+ super().__init__(status_code=response.status_code,
1153+ **response_json)
1154+
1155+
1156+class StoreSnapBuildError(StoreException):
1157+ """Exception to be raised when a snap build assertion fails"""
1158+ fmt = 'Could not assert build: {error}'
1159+
1160+ def __init__(self, response):
1161+ error = '{} {}'.format(response.status_code, response.reason)
1162+ try:
1163+ response_json = response.json()
1164+ if 'error_list' in response_json:
1165+ error = ' '.join(
1166+ error['message'] for error in response_json['error_list'])
1167+ except JSONDecodeError:
1168+ pass
1169+
1170+ super().__init__(error=error)
1171+
1172+
1173+class StoreSnapHistoryError(StoreException):
1174+ """Exception to be raised when there was a problem fetching a snap
1175+ history"""
1176+ fmt = (
1177+ 'Error fetching history of snap id {snap_id!r} for {arch!r} '
1178+ 'in {series!r} series: {error}.')
1179+
1180+ def __init__(self, response, snap_id, series, arch):
1181+ error = '{} {}'.format(response.status_code, response.reason)
1182+ try:
1183+ response_json = response.json()
1184+ if 'error_list' in response_json:
1185+ error = ' '.join(
1186+ error['message'] for error in response_json['error_list'])
1187+ except JSONDecodeError:
1188+ pass
1189+
1190+ super().__init__(
1191+ snap_id=snap_id, arch=arch or 'any arch',
1192+ series=series or 'any', error=error)
1193+
1194+
1195+class StoreSnapStatusError(StoreSnapHistoryError):
1196+ """Exception to be raised when there was a problem fetching a snap
1197+ status"""
1198+ fmt = (
1199+ 'Error fetching status of snap id {snap_id!r} for {arch!r} '
1200+ 'in {series!r} series: {error}.')
1201+
1202+
1203+class StoreChannelClosingError(StoreException):
1204+ """Exception to be raised when there was a problem fetching a snap
1205+ history"""
1206+ fmt = 'Could not close channel: {error}'
1207+
1208+ def __init__(self, response):
1209+ try:
1210+ _e = response.json()['error_list'][0]
1211+ error = '{}'.format(_e['message'])
1212+ except (JSONDecodeError, KeyError, IndexError):
1213+ error = '{} {}'.format(
1214+ response.status_code, response.reason)
1215+ super().__init__(error=error)
1216
1217=== added file 'snappy_ecosystem_tests/helpers/store_apis/rest_apis.py'
1218--- snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 1970-01-01 00:00:00 +0000
1219+++ snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 2017-02-20 17:48:06 +0000
1220@@ -0,0 +1,880 @@
1221+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
1222+#
1223+# Copyright (C) 2017 Canonical Ltd
1224+#
1225+# This program is free software: you can redistribute it and/or modify
1226+# it under the terms of the GNU General Public License version 3 as
1227+# published by the Free Software Foundation.
1228+#
1229+# This program is distributed in the hope that it will be useful,
1230+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1231+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1232+# GNU General Public License for more details.
1233+#
1234+# You should have received a copy of the GNU General Public License
1235+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1236+
1237+"""Clients needed to interact with the Snap Store"""
1238+
1239+import contextlib
1240+import hashlib
1241+import itertools
1242+import json
1243+import logging
1244+import os
1245+import urllib.parse
1246+from queue import Queue
1247+from threading import Thread
1248+from time import sleep
1249+
1250+import pymacaroons
1251+import requests
1252+from progressbar import (
1253+ AnimatedMarker,
1254+ ProgressBar,
1255+ UnknownLength,
1256+)
1257+from simplejson.scanner import JSONDecodeError
1258+from snappy_ecosystem_tests.commons.config import CONFIG_STACK
1259+
1260+from snappy_ecosystem_tests.helpers.snapcraft import constants
1261+from snappy_ecosystem_tests.helpers.snapcraft.config import SnapcraftConfig
1262+from snappy_ecosystem_tests.helpers.snapcraft.indicators import (
1263+ download_requests_stream
1264+)
1265+from snappy_ecosystem_tests.helpers.snapcraft.options import ProjectOptions
1266+from snappy_ecosystem_tests.helpers.store_apis import errors
1267+from snappy_ecosystem_tests.helpers.store_apis.upload import upload_files
1268+
1269+LOGGER = logging.getLogger(__name__)
1270+
1271+JSON_CONTENT_TYPE = {'Content-Type': 'application/json'}
1272+JSON_ACCEPT = {'Accept': 'application/json'}
1273+JSON_HEADERS = dict(JSON_CONTENT_TYPE, **JSON_ACCEPT)
1274+
1275+
1276+def _macaroon_auth(conf):
1277+ """Format a macaroon and its associated discharge.
1278+ :return: A string suitable to use in an Authorization header.
1279+ """
1280+ root_macaroon_raw = conf.get('macaroon')
1281+ if root_macaroon_raw is None:
1282+ raise errors.InvalidCredentialsError(
1283+ 'Root macaroon not in the config file')
1284+ unbound_raw = conf.get('unbound_discharge')
1285+ if unbound_raw is None:
1286+ raise errors.InvalidCredentialsError(
1287+ 'Unbound discharge not in the config file')
1288+
1289+ root_macaroon = _deserialize_macaroon(root_macaroon_raw)
1290+ unbound = _deserialize_macaroon(unbound_raw)
1291+ bound = root_macaroon.prepare_for_request(unbound)
1292+ discharge_macaroon_raw = bound.serialize()
1293+ auth = 'Macaroon root={}, discharge={}'.format(
1294+ root_macaroon_raw, discharge_macaroon_raw)
1295+ return auth
1296+
1297+
1298+def _deserialize_macaroon(value):
1299+ """Deserialize the given macaroon and return it
1300+ :raise InvalidCredentialsError: when macaroon cannot be deserialized
1301+ """
1302+ try:
1303+ return pymacaroons.Macaroon.deserialize(value)
1304+ except:
1305+ raise errors.InvalidCredentialsError('Failed to deserialize macaroon')
1306+
1307+
1308+class Client:
1309+ """A base class to define clients for the ols servers.
1310+ This is a simple wrapper around requests.Session so we inherit all good
1311+ bits while providing a simple point for tests to override when needed.
1312+ """
1313+
1314+ def __init__(self, conf, root_url):
1315+ self.conf = conf
1316+ self.root_url = root_url
1317+ self.session = requests.Session()
1318+
1319+ def request(self, method, url, params=None, headers=None, **kwargs):
1320+ """Overriding base class to handle the root url."""
1321+ # Note that url may be absolute in which case 'root_url' is ignored by
1322+ # urljoin.
1323+ if headers is None:
1324+ headers = JSON_HEADERS
1325+ final_url = urllib.parse.urljoin(self.root_url, url)
1326+ response = self.session.request(
1327+ method, final_url, headers=headers,
1328+ params=params, **kwargs)
1329+ return response
1330+
1331+ def get(self, url, **kwargs):
1332+ """Do a GET request and return the response"""
1333+ return self.request('GET', url, **kwargs)
1334+
1335+ def post(self, url, **kwargs):
1336+ """Do a POST request and return the response"""
1337+ return self.request('POST', url, **kwargs)
1338+
1339+ def put(self, url, **kwargs):
1340+ """Do a PUT request and return the response"""
1341+ return self.request('PUT', url, **kwargs)
1342+
1343+
1344+class Store:
1345+ """It is an entry point for all the REST operations in the store.
1346+ It will call the corresponding client to resolve the request.
1347+ It also takes care about refreshing the macaroon if necessary
1348+ in each request.
1349+ """
1350+
1351+ ACLs = ('package_upload', 'package_access')
1352+
1353+ def __init__(self):
1354+ self.conf = SnapcraftConfig()
1355+ self.sso = SSOClient(self.conf)
1356+ self.cpi = SnapIndexClient(self.conf)
1357+ self.updown = UpDownClient(self.conf)
1358+ self.sca = SCAClient(self.conf)
1359+
1360+ def login(self, email, password, one_time_password=None, acls=ACLs,
1361+ packages=None, channels=None, save=True):
1362+ """Log in via the Ubuntu One SSO API."""
1363+ # Ask the store for the needed capabilities to be associated with the
1364+ # macaroon.
1365+ macaroon = self.sca.get_macaroon(acls, packages, channels)
1366+ caveat_id = self._extract_caveat_id(macaroon)
1367+ unbound_discharge = self.sso.get_unbound_discharge(
1368+ email, password, one_time_password, caveat_id)
1369+ # The macaroon has been discharged, save it in the config
1370+ self.conf.set('macaroon', macaroon)
1371+ self.conf.set('unbound_discharge', unbound_discharge)
1372+ if save:
1373+ self.conf.save()
1374+
1375+ @property
1376+ def config(self):
1377+ """Get current config"""
1378+ return self.conf
1379+
1380+ def load_config(self):
1381+ """Load current config"""
1382+ self.conf.load()
1383+
1384+ def _extract_caveat_id(self, root_macaroon):
1385+ """Extract caveat id for the given macaroon
1386+ :raise InvalidCredentialsError: when given macaroon is not valid
1387+ """
1388+ macaroon = pymacaroons.Macaroon.deserialize(root_macaroon)
1389+ # macaroons are all bytes, never strings
1390+ sso_host = urllib.parse.urlparse(self.sso.root_url).netloc
1391+ for caveat in macaroon.caveats:
1392+ if caveat.location == sso_host:
1393+ return caveat.caveat_id
1394+ raise errors.InvalidCredentialsError('Invalid root macaroon')
1395+
1396+ def logout(self):
1397+ """Logout from Store client"""
1398+ self.conf.clear()
1399+ self.conf.save()
1400+
1401+ def _refresh_if_necessary(self, func, *args, **kwargs):
1402+ """Make a request, refreshing macaroons if necessary."""
1403+ try:
1404+ return func(*args, **kwargs)
1405+ except errors.StoreMacaroonNeedsRefreshError:
1406+ unbound_discharge = self.sso.refresh_unbound_discharge(
1407+ self.conf.get('unbound_discharge'))
1408+ self.conf.set('unbound_discharge', unbound_discharge)
1409+ self.conf.save()
1410+ return func(*args, **kwargs)
1411+
1412+ def get_account_information(self):
1413+ """Get current account information"""
1414+ return self._refresh_if_necessary(self.sca.get_account_information)
1415+
1416+ def register_key(self, account_key_request):
1417+ """Register a key for the current user"""
1418+ return self._refresh_if_necessary(
1419+ self.sca.register_key, account_key_request)
1420+
1421+ def register(self, snap_name, is_private=False):
1422+ """Register a package name for the current user
1423+ First refresh macaroon if necessary"""
1424+ return self._refresh_if_necessary(
1425+ self.sca.register, snap_name, is_private, constants.DEFAULT_SERIES)
1426+
1427+ def push_precheck(self, snap_name):
1428+ """Do a snap push as a pre-check (dry_run: do not actually push)
1429+ First refresh macaroon if necessary
1430+ """
1431+ return self._refresh_if_necessary(
1432+ self.sca.snap_push_precheck, snap_name)
1433+
1434+ def push_snap_build(self, snap_id, snap_build):
1435+ """Do a snap push
1436+ First refresh macaroon if necessary
1437+ """
1438+ return self._refresh_if_necessary(
1439+ self.sca.push_snap_build, snap_id, snap_build)
1440+
1441+ def upload(self, snap_name, snap_filename):
1442+ """Upload a snap file.
1443+ :raise InvalidCredentialsError when no unbound_discharge
1444+ is found in current config
1445+ """
1446+ if not self.conf.get('unbound_discharge'):
1447+ raise errors.InvalidCredentialsError(
1448+ 'Unbound discharge not in the config file')
1449+
1450+ updown_data = upload_files(snap_filename, self.updown)
1451+
1452+ return self._refresh_if_necessary(
1453+ self.sca.snap_push_metadata, snap_name, updown_data)
1454+
1455+ def release(self, snap_name, revision, channels):
1456+ """Release a snap revision to the given channels
1457+ First refresh macaroon if necessary
1458+ """
1459+ return self._refresh_if_necessary(
1460+ self.sca.snap_release, snap_name, revision, channels)
1461+
1462+ def get_snap_history(self, snap_name, series=None, arch=None):
1463+ """Get a snap history. First refresh macaroon if necessary
1464+ :raise: SnapNotFoundError if the snap does not belong to
1465+ the current user
1466+ or if it is not found.
1467+ """
1468+ if series is None:
1469+ series = constants.DEFAULT_SERIES
1470+
1471+ account_info = self.get_account_information()
1472+ try:
1473+ snap_id = account_info['snaps'][series][snap_name]['snap-id']
1474+ except KeyError:
1475+ raise errors.SnapNotFoundError(snap_name, series=series, arch=arch)
1476+
1477+ response = self._refresh_if_necessary(
1478+ self.sca.snap_history, snap_id, series, arch)
1479+
1480+ if not response:
1481+ raise errors.SnapNotFoundError(snap_name, series=series, arch=arch)
1482+
1483+ return response
1484+
1485+ def get_snap_status(self, snap_name, series=None, arch=None):
1486+ """Get a snap status. First refresh macaroon if necessary
1487+ :raise: SnapNotFoundError if the snap does not belong
1488+ to the current user
1489+ or if it is not found.
1490+ """
1491+ if series is None:
1492+ series = constants.DEFAULT_SERIES
1493+
1494+ account_info = self.get_account_information()
1495+ try:
1496+ snap_id = account_info['snaps'][series][snap_name]['snap-id']
1497+ except KeyError:
1498+ raise errors.SnapNotFoundError(snap_name, series=series, arch=arch)
1499+
1500+ response = self._refresh_if_necessary(
1501+ self.sca.snap_status, snap_id, series, arch)
1502+
1503+ if not response:
1504+ raise errors.SnapNotFoundError(snap_name, series=series, arch=arch)
1505+
1506+ return response
1507+
1508+ def close_channels(self, snap_id, channel_names):
1509+ """Close channels for the given snap_id.
1510+ First refresh macaroon if necessary"""
1511+ return self._refresh_if_necessary(
1512+ self.sca.close_channels, snap_id, channel_names)
1513+
1514+ def download(self, snap_name, channel, download_path, arch=None):
1515+ """Download a snap and store it locally
1516+ :param snap_name: the snap name
1517+ :param channel: the channel name
1518+ :param download_path: the local path to download the snap
1519+ :param arch: the snap architecture
1520+ """
1521+ if arch is None:
1522+ arch = ProjectOptions().deb_arch
1523+
1524+ package = self.cpi.get_package(snap_name, channel, arch)
1525+ self._download_snap(
1526+ snap_name, download_path,
1527+ package['anon_download_url'], package['download_sha512'])
1528+
1529+ def _download_snap(self, name, download_path, download_url,
1530+ expected_sha512):
1531+ """Download a snap and store it locally
1532+ :param name: the snap name
1533+ :param download_path: the local path to download the snap
1534+ :param download_url: the url to download the snap
1535+ :param expected_sha512: the snap expected sha256
1536+ """
1537+ if self._is_downloaded(download_path, expected_sha512):
1538+ LOGGER.info('Already downloaded %s at %s', name, download_path)
1539+ return
1540+ LOGGER.info('Downloading %s at %s', name, download_path)
1541+ request = self.cpi.get(download_url, stream=True)
1542+ request.raise_for_status()
1543+ download_requests_stream(request, download_path)
1544+
1545+ if self._is_downloaded(download_path, expected_sha512):
1546+ LOGGER.info('Successfully downloaded %s at %s', name,
1547+ download_path)
1548+ else:
1549+ raise errors.SHAMismatchError(download_path, expected_sha512)
1550+
1551+ @staticmethod
1552+ def _is_downloaded(path, expected_sha512):
1553+ """Return True if the snap is already downloaded in the local machine
1554+ :param path: the path to look for the snap
1555+ :param expected_sha512: the expected snap sha512
1556+ """
1557+ if not os.path.exists(path):
1558+ return False
1559+
1560+ file_sum = hashlib.sha512()
1561+ with open(path, 'rb') as _f:
1562+ for file_chunk in iter(
1563+ lambda: _f.read(file_sum.block_size * 128), b''):
1564+ file_sum.update(file_chunk)
1565+ return expected_sha512 == file_sum.hexdigest()
1566+
1567+ def push_validation(self, snap_id, assertion):
1568+ """Push a snap validation/assertion to the store
1569+ :param snap_id: the snap id
1570+ :param assertion: the snap assertion
1571+ """
1572+ return self.sca.push_validation(snap_id, assertion)
1573+
1574+ def get_validations(self, snap_id):
1575+ """Get current snap validations/assertions"""
1576+ return self.sca.get_validations(snap_id)
1577+
1578+ def sign_developer_agreement(self, latest_tos_accepted=False):
1579+ """Sign developer agreement to be able to do operations in the store"""
1580+ return self.sca.sign_developer_agreement(latest_tos_accepted)
1581+
1582+
1583+class SSOClient(Client):
1584+ """The Single Sign On server deals with authentication.
1585+ It is used directly or indirectly by other servers.
1586+ """
1587+ def __init__(self, conf):
1588+ super().__init__(conf, os.environ.get(
1589+ 'UBUNTU_SSO_API_ROOT_URL',
1590+ CONFIG_STACK.config.get('production_urls', 'sso')))
1591+
1592+ def get_unbound_discharge(self, email, password, one_time_password,
1593+ caveat_id):
1594+ """Get the unbound discharge for the caveat_id.
1595+ :param email: user email
1596+ :param password: user password
1597+ :param one_time_password: the one time password
1598+ :param caveat_id: the caveat id to get the unbound discharge
1599+ :raise StoreTwoFactorAuthenticationRequired:
1600+ if response is unauthorized and two-factor is required
1601+ :raise StoreAuthenticationError: if failed to get unbound discharge
1602+ """
1603+ data = dict(email=email, password=password,
1604+ caveat_id=caveat_id)
1605+ if one_time_password:
1606+ data['otp'] = one_time_password
1607+ response = self.post(
1608+ 'tokens/discharge', data=json.dumps(data),
1609+ headers=JSON_HEADERS)
1610+ try:
1611+ response_json = response.json()
1612+ except JSONDecodeError:
1613+ response_json = {}
1614+ if response.ok:
1615+ return response_json['discharge_macaroon']
1616+ else:
1617+ if (response.status_code == requests.codes.get('unauthorized') and
1618+ any(error.get('code') == 'twofactor-required'
1619+ for error in response_json.get('error_list', []))):
1620+ raise errors.StoreTwoFactorAuthenticationRequired()
1621+ else:
1622+ raise errors.StoreAuthenticationError(
1623+ 'Failed to get unbound discharge: {}'.format(
1624+ response.text))
1625+
1626+ def refresh_unbound_discharge(self, unbound_discharge):
1627+ """Refresh the given unbound discharge.
1628+ :raise StoreAuthenticationError: if fails to refresh the
1629+ unbound discharge.
1630+ """
1631+ data = {'discharge_macaroon': unbound_discharge}
1632+ response = self.post(
1633+ 'tokens/refresh', data=json.dumps(data),
1634+ headers=JSON_HEADERS)
1635+ if response.ok:
1636+ return response.json()['discharge_macaroon']
1637+ else:
1638+ raise errors.StoreAuthenticationError(
1639+ 'Failed to refresh unbound discharge: {}'.format(
1640+ response.text))
1641+
1642+
1643+class SnapIndexClient(Client):
1644+ """The Click Package Index knows everything about existing snaps.
1645+ https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex is the
1646+ canonical reference.
1647+ """
1648+ def __init__(self, conf):
1649+ super().__init__(conf, os.environ.get(
1650+ 'UBUNTU_STORE_SEARCH_ROOT_URL',
1651+ CONFIG_STACK.config.get('production_urls', 'search')))
1652+
1653+ def get_default_headers(self):
1654+ """Return default headers for CPI requests.
1655+ Tries to build an 'Authorization' header with local credentials
1656+ if they are available.
1657+ Also pin specific branded store if `SNAPCRAFT_UBUNTU_STORE`
1658+ environment is set.
1659+ """
1660+ headers = {}
1661+
1662+ with contextlib.suppress(errors.InvalidCredentialsError):
1663+ headers['Authorization'] = _macaroon_auth(self.conf)
1664+
1665+ branded_store = os.getenv('SNAPCRAFT_UBUNTU_STORE')
1666+ if branded_store:
1667+ headers['X-Ubuntu-Store'] = branded_store
1668+
1669+ return headers
1670+
1671+ def get_package(self, snap_name, channel, arch=None):
1672+ """Get snap details.
1673+ :param snap_name: the snap name
1674+ :param channel: the snap channel
1675+ :param arch: the snap architecture
1676+ """
1677+ headers = self.get_default_headers()
1678+ headers.update({
1679+ 'Accept': 'application/hal+json',
1680+ 'X-Ubuntu-Release': constants.DEFAULT_SERIES,
1681+ })
1682+ if arch:
1683+ headers['X-Ubuntu-Architecture'] = arch
1684+
1685+ params = {
1686+ 'channel': channel,
1687+ 'fields': 'status,anon_download_url,download_url,'
1688+ 'download_sha512,snap_id,release',
1689+ }
1690+ LOGGER.info('Getting details for %s', snap_name)
1691+ url = 'api/v1/snaps/details/{}'.format(snap_name)
1692+ resp = super().get(url, headers=headers, params=params)
1693+ if resp.status_code != 200:
1694+ raise errors.SnapNotFoundError(snap_name, channel, arch)
1695+ return resp.json()
1696+
1697+
1698+class UpDownClient(Client):
1699+ """The Up/Down server provide upload/download snap capabilities."""
1700+
1701+ def __init__(self, conf):
1702+ super().__init__(conf, os.environ.get(
1703+ 'UBUNTU_STORE_UPLOAD_ROOT_URL',
1704+ CONFIG_STACK.config.get('production_urls', 'upload')))
1705+
1706+ def upload(self, monitor):
1707+ """Upload a snap capability"""
1708+ return self.post(
1709+ urllib.parse.urljoin(self.root_url, 'unscanned-upload/'),
1710+ data=monitor,
1711+ headers=dict({'Content-Type': monitor.content_type},
1712+ **JSON_ACCEPT))
1713+
1714+
1715+class SCAClient(Client):
1716+ """The software center agent deals with managing snaps."""
1717+
1718+ def __init__(self, conf):
1719+ super().__init__(conf, os.environ.get(
1720+ 'UBUNTU_STORE_API_ROOT_URL',
1721+ CONFIG_STACK.config.get('production_urls', 'root_api')))
1722+
1723+ def get_macaroon(self, acls, packages=None, channels=None):
1724+ """Get a macaroon that is used to authenticate
1725+ request against the store
1726+ :param acls: list of permissions to request the macaroon for.
1727+ :param packages: list of packages to request the macaroon for.
1728+ :param channels: list of channels to request the macaroon for.
1729+ """
1730+ data = {'permissions': acls}
1731+ if packages is not None:
1732+ data.update({'packages': packages})
1733+ if channels is not None:
1734+ data.update({'channels': channels})
1735+ headers = JSON_ACCEPT
1736+ response = self.post(
1737+ 'acl/', json=data, headers=headers)
1738+ if response.ok:
1739+ return response.json()['macaroon']
1740+ else:
1741+ raise errors.StoreAuthenticationError('Failed to get macaroon')
1742+
1743+ @staticmethod
1744+ def _needs_refreshed_response(response):
1745+ """Return True if the given response needs to refresh the macaroon.
1746+ :param response: the response to check if the
1747+ macaroon refresh is needed.
1748+ """
1749+ return (
1750+ response.status_code == requests.codes.get('unauthorized') and
1751+ response.headers.get('WWW-Authenticate') == (
1752+ 'Macaroon needs_refresh=1'))
1753+
1754+ def request(self, *args, **kwargs):
1755+ """Wrapper of client request that raises an exception for
1756+ the cases that the response macaroon needs to be refreshed
1757+ :raise StoreMacaroonNeedsRefreshError:
1758+ if the response needs the macaroon to be refreshed.
1759+ """
1760+ response = super().request(*args, **kwargs)
1761+ if self._needs_refreshed_response(response):
1762+ raise errors.StoreMacaroonNeedsRefreshError()
1763+ return response
1764+
1765+ def get_account_information(self):
1766+ """Get the current account information
1767+ :raise StoreAccountInformationError: if response is not ok status
1768+ """
1769+ auth = _macaroon_auth(self.conf)
1770+ response = self.get(
1771+ 'account',
1772+ headers=dict({'Authorization': auth}, **JSON_ACCEPT))
1773+ if response.ok:
1774+ return response.json()
1775+ else:
1776+ raise errors.StoreAccountInformationError(response)
1777+
1778+ def register_key(self, account_key_request):
1779+ """Register a key for a user with the given request
1780+ :param account_key_request: the key to register
1781+ :raise StoreKeyRegistrationError
1782+ """
1783+ data = {'account_key_request': account_key_request}
1784+ auth = _macaroon_auth(self.conf)
1785+ response = self.post(
1786+ 'account/account-key', data=json.dumps(data),
1787+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1788+ if not response.ok:
1789+ raise errors.StoreKeyRegistrationError(response)
1790+
1791+ def register(self, snap_name, is_private, series):
1792+ """Register a snap name in the store
1793+ :param snap_name: the name to be registered
1794+ :param is_private: whether the snap name is private or not
1795+ :param series: the snap's series to be registered
1796+ """
1797+ auth = _macaroon_auth(self.conf)
1798+ data = dict(snap_name=snap_name, is_private=is_private,
1799+ series=series)
1800+ response = self.post(
1801+ 'register-name/', data=json.dumps(data),
1802+ headers=dict({'Authorization': auth}, **JSON_ACCEPT))
1803+ if not response.ok:
1804+ raise errors.StoreRegistrationError(snap_name, response)
1805+
1806+ def snap_push_precheck(self, snap_name):
1807+ """Do a snap push pre-check (dry_run: do not actually push)
1808+ :raise StorePushError
1809+ """
1810+ data = {
1811+ 'name': snap_name,
1812+ 'dry_run': True,
1813+ }
1814+ auth = _macaroon_auth(self.conf)
1815+ response = self.post(
1816+ 'snap-push/', data=json.dumps(data),
1817+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1818+ if not response.ok:
1819+ raise errors.StorePushError(data['name'], response)
1820+
1821+ def snap_push_metadata(self, snap_name, updown_data):
1822+ """Push metadata fot the given snap
1823+ :param snap_name: the snap name which metadata want to be updated
1824+ :param updown_data: a dictionary that contains the metadata: upload_id,
1825+ binary_filesize, source_uploaded
1826+ :raise: StorePushError
1827+ """
1828+ data = {
1829+ 'name': snap_name,
1830+ 'series': constants.DEFAULT_SERIES,
1831+ 'updown_id': updown_data['upload_id'],
1832+ 'binary_filesize': updown_data['binary_filesize'],
1833+ 'source_uploaded': updown_data['source_uploaded'],
1834+ }
1835+ auth = _macaroon_auth(self.conf)
1836+ response = self.post(
1837+ 'snap-push/', data=json.dumps(data),
1838+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1839+ if not response.ok:
1840+ raise errors.StorePushError(data['name'], response)
1841+
1842+ return StatusTracker(response.json()['status_details_url'])
1843+
1844+ def snap_release(self, snap_name, revision, channels):
1845+ """
1846+ Release a snap revision to the given channel
1847+ :param snap_name: the snap name to be released
1848+ :param revision: the revision number to be released
1849+ :param channels: the channels in which release the snap
1850+ :raise: StoreReleaseError
1851+ """
1852+ data = {
1853+ 'name': snap_name,
1854+ 'revision': str(revision),
1855+ 'channels': channels,
1856+ }
1857+ auth = _macaroon_auth(self.conf)
1858+ response = self.post(
1859+ 'snap-release/', data=json.dumps(data),
1860+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1861+ if not response.ok:
1862+ raise errors.StoreReleaseError(data['name'], response)
1863+
1864+ response_json = response.json()
1865+
1866+ return response_json
1867+
1868+ def push_validation(self, snap_id, assertion):
1869+ """
1870+ Push a validation/assertion to the store
1871+ :param snap_id: the snap_id of the snap
1872+ :param assertion: the validation/assertion to be pushed
1873+ :raise: JSONDecodeError, StoreValidationError
1874+ """
1875+ data = {
1876+ 'assertion': assertion.decode('utf-8'),
1877+ }
1878+ auth = _macaroon_auth(self.conf)
1879+ response = self.put(
1880+ 'snaps/{}/validations'.format(snap_id), data=json.dumps(data),
1881+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1882+ if not response.ok:
1883+ raise errors.StoreValidationError(snap_id, response)
1884+ try:
1885+ response_json = response.json()
1886+ except JSONDecodeError:
1887+ message = ('Invalid response from the server when pushing '
1888+ 'validations: {} {}').format(
1889+ response.status_code, response)
1890+ LOGGER.debug(message)
1891+ raise errors.StoreValidationError(
1892+ response, message='Invalid response from the server')
1893+
1894+ return response_json
1895+
1896+ def get_validations(self, snap_id):
1897+ """Get validations/assertion of a given snap
1898+ :param snap_id: the id of the snap to get the validations for.
1899+ :raise: JSONDecodeError, StoreValidationError
1900+ """
1901+ auth = _macaroon_auth(self.conf)
1902+ response = self.get(
1903+ 'snaps/{}/validations'.format(snap_id),
1904+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1905+ if not response.ok:
1906+ raise errors.StoreValidationError(snap_id, response)
1907+ try:
1908+ response_json = response.json()
1909+ except JSONDecodeError:
1910+ message = ('Invalid response from the server when getting '
1911+ 'validations: {} {}').format(
1912+ response.status_code, response)
1913+ LOGGER.debug(message)
1914+ raise errors.StoreValidationError(
1915+ response, message='Invalid response from the server')
1916+
1917+ return response_json
1918+
1919+ def push_snap_build(self, snap_id, snap_build):
1920+ """
1921+ Push a snap build to the store
1922+ :param snap_id: the snap id
1923+ :param snap_build: the snap build to push
1924+ :raise: StoreSnapBuildError
1925+ """
1926+ url = 'snaps/{}/builds'.format(snap_id)
1927+ data = json.dumps({"assertion": snap_build})
1928+
1929+ headers = dict({'Authorization': _macaroon_auth(self.conf)},
1930+ **JSON_CONTENT_TYPE)
1931+ response = self.post(url, data=data, headers=headers)
1932+ if not response.ok:
1933+ raise errors.StoreSnapBuildError(response)
1934+
1935+ def snap_history(self, snap_id, series=None, arch=None):
1936+ """
1937+ Get a snap history
1938+ :param snap_id: the id of the snap to get the history for.
1939+ :param series: the series of the snap
1940+ :param arch: the architecture of the snap
1941+ :raise: StoreSnapHistoryError
1942+ """
1943+ qs = {}
1944+ if series:
1945+ qs['series'] = series
1946+ if arch:
1947+ qs['arch'] = arch
1948+ url = 'snaps/' + snap_id + '/history'
1949+ if qs:
1950+ url += '?' + urllib.parse.urlencode(qs)
1951+ auth = _macaroon_auth(self.conf)
1952+ response = self.get(
1953+ url,
1954+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1955+ if not response.ok:
1956+ raise errors.StoreSnapHistoryError(response, snap_id, series, arch)
1957+
1958+ response_json = response.json()
1959+
1960+ return response_json
1961+
1962+ def snap_status(self, snap_id, series=None, arch=None):
1963+ """
1964+ Get a snap status
1965+ :param snap_id: the id of the snap to get the history for.
1966+ :param series: the series of the snap
1967+ :param arch: the architecture of the snap
1968+ :raise: StoreSnapStatusError
1969+ """
1970+ qs = {}
1971+ if series:
1972+ qs['series'] = series
1973+ if arch:
1974+ qs['arch'] = arch
1975+ url = 'snaps/' + snap_id + '/status'
1976+ if qs:
1977+ url += '?' + urllib.parse.urlencode(qs)
1978+ auth = _macaroon_auth(self.conf)
1979+ response = self.get(
1980+ url,
1981+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
1982+ if not response.ok:
1983+ raise errors.StoreSnapStatusError(response, snap_id, series, arch)
1984+
1985+ response_json = response.json()
1986+
1987+ return response_json
1988+
1989+ def close_channels(self, snap_id, channel_names):
1990+ """
1991+ Close channel for a given snap
1992+ :param snap_id: the id of the snap
1993+ :param channel_names: the channels to be closed
1994+ :raise: StoreChannelClosingError
1995+ """
1996+ url = 'snaps/{}/close'.format(snap_id)
1997+ data = {
1998+ 'channels': channel_names
1999+ }
2000+ headers = {
2001+ 'Authorization': _macaroon_auth(self.conf),
2002+ }
2003+ response = self.post(url, json=data, headers=headers)
2004+ if not response.ok:
2005+ raise errors.StoreChannelClosingError(response)
2006+
2007+ try:
2008+ results = response.json()
2009+ return results['closed_channels'], results['channel_maps']
2010+ except (JSONDecodeError, KeyError):
2011+ LOGGER.debug(
2012+ 'Invalid response from the server on channel closing:\n'
2013+ '%s %s\n%s', response.status_code, response.reason,
2014+ response.content)
2015+ raise errors.StoreChannelClosingError(response)
2016+
2017+ def sign_developer_agreement(self, latest_tos_accepted=False):
2018+ """Sign developer agreement to be able to do operations in the store"""
2019+ auth = _macaroon_auth(self.conf)
2020+ data = {'latest_tos_accepted': latest_tos_accepted}
2021+ response = self.post(
2022+ 'agreement/', data=json.dumps(data),
2023+ headers=dict({'Authorization': auth}, **JSON_HEADERS))
2024+ if not response.ok:
2025+ raise errors.DeveloperAgreementSignError(response)
2026+ return response.json()
2027+
2028+
2029+class StatusTracker:
2030+
2031+ """Class to track the status of an operation"""
2032+
2033+ __messages = {
2034+ 'being_processed': 'Processing...',
2035+ 'ready_to_release': 'Ready to release!',
2036+ 'need_manual_review': 'Will need manual review...',
2037+ 'processing_error': 'Error while processing...',
2038+ }
2039+
2040+ __error_codes = (
2041+ 'processing_error',
2042+ 'need_manual_review',
2043+ )
2044+
2045+ def __init__(self, status_details_url):
2046+ self.__content = None
2047+ self.__status_details_url = status_details_url
2048+
2049+ def track(self):
2050+ """Track the status"""
2051+ queue = Queue()
2052+ thread = Thread(target=self._update_status, args=(queue,))
2053+ thread.start()
2054+ widgets = ['Processing...', AnimatedMarker()]
2055+ progress_indicator = ProgressBar(widgets=widgets, maxval=UnknownLength)
2056+ progress_indicator.start()
2057+ content = {}
2058+ for indicator_count in itertools.count():
2059+ if not queue.empty():
2060+ content = queue.get()
2061+ if isinstance(content, Exception):
2062+ raise Exception(content)
2063+ widgets[0] = self._get_message(content)
2064+ progress_indicator.update(indicator_count)
2065+ if content.get('processed'):
2066+ break
2067+ sleep(0.1)
2068+ progress_indicator.finish()
2069+ self.__content = content
2070+ return content
2071+
2072+ def raise_for_code(self):
2073+ """Raise an exception if content has particular error codes"""
2074+ if any(self.__content['code'] == _k for _k in self.__error_codes):
2075+ raise errors.StoreReviewError(self.__content)
2076+
2077+ def _get_message(self, content):
2078+ """Get a message from content"""
2079+ return self.__messages.get(content['code'], content['code'])
2080+
2081+ def _update_status(self, queue):
2082+ """Update an operation status"""
2083+ for content in self._get_status():
2084+ queue.put(content)
2085+ if content['processed']:
2086+ break
2087+ sleep(constants.SCAN_STATUS_POLL_DELAY)
2088+
2089+ def _get_status(self):
2090+ """Get an operation status"""
2091+ connection_errors_allowed = 10
2092+ while True:
2093+ try:
2094+ content = requests.get(self.__status_details_url).json()
2095+ except (requests.ConnectionError, requests.HTTPError) as _e:
2096+ if not connection_errors_allowed:
2097+ yield _e
2098+ content = {'processed': False, 'code': 'being_processed'}
2099+ connection_errors_allowed -= 1
2100+ yield content
2101
2102=== added file 'snappy_ecosystem_tests/helpers/store_apis/upload.py'
2103--- snappy_ecosystem_tests/helpers/store_apis/upload.py 1970-01-01 00:00:00 +0000
2104+++ snappy_ecosystem_tests/helpers/store_apis/upload.py 2017-02-20 17:48:06 +0000
2105@@ -0,0 +1,97 @@
2106+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2107+
2108+#
2109+# Snappy Ecosystem Tests
2110+# Copyright (C) 2017 Canonical
2111+#
2112+# This program is free software: you can redistribute it and/or modify
2113+# it under the terms of the GNU General Public License as published by
2114+# the Free Software Foundation, either version 3 of the License, or
2115+# (at your option) any later version.
2116+#
2117+# This program is distributed in the hope that it will be useful,
2118+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2119+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2120+# GNU General Public License for more details.
2121+#
2122+# You should have received a copy of the GNU General Public License
2123+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2124+#
2125+
2126+"""Helpers to upload binary files to the store"""
2127+
2128+import logging
2129+import functools
2130+import os
2131+
2132+from progressbar import (
2133+ Bar,
2134+ Percentage,
2135+ ProgressBar,
2136+)
2137+from requests_toolbelt import (MultipartEncoder, MultipartEncoderMonitor)
2138+
2139+from snappy_ecosystem_tests.helpers.store_apis.errors import StoreUploadError
2140+
2141+LOGGER = logging.getLogger(__name__)
2142+
2143+
2144+def _update_progress_bar(progress_bar, maximum_value, monitor):
2145+ """Update the progress bar status"""
2146+ if monitor.bytes_read <= maximum_value:
2147+ progress_bar.update(monitor.bytes_read)
2148+
2149+
2150+def upload_files(binary_filename, updown_client):
2151+ """Upload a binary file to the Store.
2152+ Submit a file to the Store upload service and return the
2153+ corresponding upload_id.
2154+ """
2155+ binary_file = None
2156+ try:
2157+ binary_file_size = os.path.getsize(binary_filename)
2158+ binary_file = open(binary_filename, 'rb')
2159+ encoder = MultipartEncoder(
2160+ fields={
2161+ 'binary': ('filename', binary_file, 'application/octet-stream')
2162+ }
2163+ )
2164+
2165+ # Create a progress bar that looks like: Uploading foo [== ] 50%
2166+ progress_bar = ProgressBar(
2167+ widgets=['Uploading {} '.format(binary_filename),
2168+ Bar(marker='=', left='[', right=']'), ' ', Percentage()],
2169+ maxval=os.path.getsize(binary_filename))
2170+ progress_bar.start()
2171+ # Print a newline so the progress bar has some breathing room.
2172+ LOGGER.info('')
2173+
2174+ # Create a monitor for this upload, so that progress can be displayed
2175+ monitor = MultipartEncoderMonitor(
2176+ encoder, functools.partial(_update_progress_bar, progress_bar,
2177+ binary_file_size))
2178+
2179+ # Begin upload
2180+ response = updown_client.upload(monitor)
2181+
2182+ # Make sure progress bar shows 100% complete
2183+ progress_bar.finish()
2184+
2185+ except Exception as err:
2186+ raise RuntimeError(
2187+ 'An unexpected error was found while uploading '
2188+ 'files: {!r}.'.format(err))
2189+ finally:
2190+ # Close the open file
2191+ if binary_file:
2192+ binary_file.close()
2193+
2194+ if not response.ok:
2195+ raise StoreUploadError(response)
2196+
2197+ response_data = response.json()
2198+ return {
2199+ 'upload_id': response_data['upload_id'],
2200+ 'binary_filesize': binary_file_size,
2201+ 'source_uploaded': False,
2202+ }
2203
2204=== removed directory 'snappy_ecosystem_tests/snapcraft'
2205=== removed file 'snappy_ecosystem_tests/snapcraft/__init__.py'
2206--- snappy_ecosystem_tests/snapcraft/__init__.py 2017-02-09 20:09:40 +0000
2207+++ snappy_ecosystem_tests/snapcraft/__init__.py 1970-01-01 00:00:00 +0000
2208@@ -1,19 +0,0 @@
2209-# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2210-
2211-#
2212-# Snappy Ecosystem Tests
2213-# Copyright (C) 2017 Canonical
2214-#
2215-# This program is free software: you can redistribute it and/or modify
2216-# it under the terms of the GNU General Public License as published by
2217-# the Free Software Foundation, either version 3 of the License, or
2218-# (at your option) any later version.
2219-#
2220-# This program is distributed in the hope that it will be useful,
2221-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2222-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2223-# GNU General Public License for more details.
2224-#
2225-# You should have received a copy of the GNU General Public License
2226-# along with this program. If not, see <http://www.gnu.org/licenses/>.
2227-#
2228
2229=== removed file 'snappy_ecosystem_tests/snapcraft/snapcraft.py'
2230--- snappy_ecosystem_tests/snapcraft/snapcraft.py 2017-02-15 19:14:19 +0000
2231+++ snappy_ecosystem_tests/snapcraft/snapcraft.py 1970-01-01 00:00:00 +0000
2232@@ -1,74 +0,0 @@
2233-# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2234-
2235-#
2236-# Snappy Ecosystem Tests
2237-# Copyright (C) 2017 Canonical
2238-#
2239-# This program is free software: you can redistribute it and/or modify
2240-# it under the terms of the GNU General Public License as published by
2241-# the Free Software Foundation, either version 3 of the License, or
2242-# (at your option) any later version.
2243-#
2244-# This program is distributed in the hope that it will be useful,
2245-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2246-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2247-# GNU General Public License for more details.
2248-#
2249-# You should have received a copy of the GNU General Public License
2250-# along with this program. If not, see <http://www.gnu.org/licenses/>.
2251-#
2252-
2253-"""Snapcraft client helpers"""
2254-
2255-import pexpect
2256-
2257-
2258-# login credentials exported by shell environment
2259-from snappy_ecosystem_tests.utils import storeconfig
2260-
2261-LOGIN_EMAIL, LOGIN_PASSWORD = storeconfig.get_store_credentials()
2262-
2263-
2264-class Snapcraft(object):
2265- """Contain Snapcraft specific functionality to use via command
2266- line interface"""
2267- def __init__(self):
2268- """Create new snapcraft instance."""
2269- self._login = False
2270- self._cleanup()
2271-
2272- def _cleanup(self):
2273- """Perform cleanup actions"""
2274- self.logout()
2275-
2276- def logout(self):
2277- """logout of snapcraft store session"""
2278- child = pexpect.spawn("snapcraft logout")
2279- err = child.expect('Credentials cleared.')
2280- child.terminate(True)
2281- child.wait()
2282- if err is not 0:
2283- raise ValueError("Failed to logout")
2284-
2285- self._login = False
2286-
2287- def login(self):
2288- """login to store using the credential environment variables"""
2289- child = pexpect.spawn("snapcraft login")
2290- child.expect('Email: ')
2291- child.sendline(LOGIN_EMAIL)
2292- child.expect('Password:')
2293- child.sendline(LOGIN_PASSWORD)
2294- err = child.expect('Login successful')
2295- if err is not 0:
2296- raise ValueError("Failed to login")
2297-
2298- self._login = True
2299-
2300- def list_registered(self):
2301- """call snapcraft list-registered command,
2302- raise exception if not logged in"""
2303- if self._login is False:
2304- raise ValueError("User is not logged in, "
2305- "please login before using this command")
2306- return pexpect.spawnu("snapcraft list-registered").read()
2307
2308=== added file 'snappy_ecosystem_tests/tests/test_store_apis_login.py'
2309--- snappy_ecosystem_tests/tests/test_store_apis_login.py 1970-01-01 00:00:00 +0000
2310+++ snappy_ecosystem_tests/tests/test_store_apis_login.py 2017-02-20 17:48:06 +0000
2311@@ -0,0 +1,39 @@
2312+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2313+
2314+#
2315+# Snappy Ecosystem Tests
2316+# Copyright (C) 2017 Canonical
2317+#
2318+# This program is free software: you can redistribute it and/or modify
2319+# it under the terms of the GNU General Public License as published by
2320+# the Free Software Foundation, either version 3 of the License, or
2321+# (at your option) any later version.
2322+#
2323+# This program is distributed in the hope that it will be useful,
2324+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2325+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2326+# GNU General Public License for more details.
2327+#
2328+# You should have received a copy of the GNU General Public License
2329+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2330+#
2331+
2332+"""Test for Login to the Store via RESTful APIs"""
2333+
2334+from snappy_ecosystem_tests.helpers.store_apis.rest_apis import Store
2335+from snappy_ecosystem_tests.helpers.test_base import SnappyEcosystemTestCase
2336+from snappy_ecosystem_tests.utils.storeconfig import get_store_credentials
2337+
2338+
2339+class StoreAPILoginTestCase(SnappyEcosystemTestCase):
2340+
2341+ def setUp(self):
2342+ super().setUp()
2343+ self.client = Store()
2344+
2345+ def test_api_login_logout(self):
2346+ """Test login and logout functionality in store REST APIs"""
2347+ self.client.login(*get_store_credentials())
2348+ self.client.load_config()
2349+ self.assertIsNotNone(self.client.config.get('macaroon'))
2350+ self.assertIsNotNone(self.client.config.get('unbound_discharge'))
2351
2352=== modified file 'snappy_ecosystem_tests/tests/test_store_login.py'
2353--- snappy_ecosystem_tests/tests/test_store_login.py 2017-02-15 19:14:19 +0000
2354+++ snappy_ecosystem_tests/tests/test_store_login.py 2017-02-20 17:48:06 +0000
2355@@ -20,7 +20,7 @@
2356
2357 """Test for Login to the Store via Web Interface, snapcraft and snapd"""
2358
2359-from snappy_ecosystem_tests.snapcraft.snapcraft import Snapcraft
2360+from snappy_ecosystem_tests.helpers.snapcraft.client import Snapcraft
2361 from snappy_ecosystem_tests.helpers.web_test_base import UbuntuStoreWebTestsBase
2362
2363

Subscribers

People subscribed via source and target branches