Merge ~cjwatson/snapstore-client:webservices-tests into snapstore-client:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: df5f4386a55b0a3e42c21b4d8a99db64ba963937
Merged at revision: df5f4386a55b0a3e42c21b4d8a99db64ba963937
Proposed branch: ~cjwatson/snapstore-client:webservices-tests
Merge into: snapstore-client:master
Prerequisite: ~cjwatson/snapstore-client:better-logging
Diff against target: 738 lines (+697/-2)
4 files modified
requirements-dev.txt (+4/-0)
snapstore_client/tests/factory.py (+252/-0)
snapstore_client/tests/test_webservices.py (+439/-0)
snapstore_client/webservices.py (+2/-2)
Reviewer Review Type Date Requested Status
Matt Goodall (community) Approve
Review via email: mp+326159@code.launchpad.net

Commit message

Add webservices tests

To post a comment you must log in.
Revision history for this message
Matt Goodall (matt-goodall) wrote :

looks good. some minor comments, mostly optional and/or pedantic :)

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) wrote :

Fixed most of that except where commented, thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/requirements-dev.txt b/requirements-dev.txt
2index 49a3c2a..c29ab6e 100644
3--- a/requirements-dev.txt
4+++ b/requirements-dev.txt
5@@ -1,4 +1,8 @@
6+acceptable>=0.9
7 coverage
8 fixtures
9 flake8
10+pysha3>=1.0
11+responses
12+snapstore-schemas
13 testtools
14diff --git a/snapstore_client/tests/factory.py b/snapstore_client/tests/factory.py
15new file mode 100644
16index 0000000..e733cfa
17--- /dev/null
18+++ b/snapstore_client/tests/factory.py
19@@ -0,0 +1,252 @@
20+# Copyright 2017 Canonical Ltd.
21+
22+"""Test Factory.
23+
24+Since siab-client does not have any database, its persistence layer consists
25+of making requests to other services in the store.
26+
27+The test factory therefore makes it easy to create payloads that are
28+consistent with what those other services return from their various API
29+endpoints.
30+
31+Factory functions are grouped into classes, one for each service.
32+"""
33+
34+import random
35+import string
36+
37+
38+SNAP_ID_ALPHABET = string.ascii_letters + string.digits
39+
40+
41+def generate_snap_id():
42+ """Generate a random ID to identify a snap entity.
43+
44+ The id is a sequence of 32 random characters taken out of an alphabet
45+ of 62 characters (uppercase letters + lowercase letters + numbers), for
46+ a total of 310 bits of space. These unique identifiers are generated
47+ centrally or delegated to known parties.
48+
49+ This function does not check for duplicates.
50+
51+ """
52+ return ''.join(random.choice(SNAP_ID_ALPHABET) for _ in range(32))
53+
54+
55+class APIError(Exception):
56+ """The base class for all user-visible exceptions.
57+
58+ The snapident presentation layer knows how to transform instances of this
59+ exception to HTTP responses in a common format. This exception is designed
60+ to contain one *or more* error messages. The exception can be created with
61+ a list of messages and an HTTP status code:
62+
63+ >>> raise APIError(["error one", "error two", "error three"])
64+
65+ This is useful when the full list of errors to raise is known at once.
66+
67+ However, a second approach exists, which is to create an empty APIError
68+ instance, and iteratively add errors over time:
69+
70+ >>> errors = APIError.empty()
71+ >>> for thing in list:
72+ ... if some_error_condition:
73+ ... errors.add_error("Some error message")
74+ ...
75+ >>> if errors:
76+ ... raise errors
77+
78+ The 'truthieness' of the exception instance is determined by at least one
79+ error message having been added.
80+
81+ Another common case is needing to raise a single error. It's mildly
82+ inconvenient to have to specify a single-item list all the time, so there's
83+ a convenience function to make that easier:
84+
85+ >>> raise APIError.single("Some message")
86+ """
87+
88+ status_code = 400
89+
90+ def __init__(self, error_list):
91+ self._error_list = error_list
92+
93+ def add_error(self, error_message):
94+ self._error_list.append(error_message)
95+
96+ def __bool__(self):
97+ return len(self._error_list) > 0
98+
99+ def to_dict(self):
100+ return {'error_list': [{'message': m} for m in self._error_list]}
101+
102+ @classmethod
103+ def single(cls, message):
104+ return cls([message])
105+
106+ @classmethod
107+ def empty(cls):
108+ return cls([])
109+
110+
111+class SnapIdentBuilder:
112+
113+ def __init__(self):
114+ self.snaps = []
115+
116+ def get_payload(self):
117+ return {'snaps': self.snaps}
118+
119+ def add_snap(self, snap_name=None, snap_id=None, series=None,
120+ authority=None, private=None, publisher_id=None, stores=None,
121+ status=None, country_blacklist=None, country_whitelist=None,
122+ blob=None):
123+ snap_name = snap_name or 'special-sauce'
124+ snap_id = snap_id or generate_snap_id()
125+ self.snaps.append({
126+ 'snap_id': snap_id,
127+ 'snap_name': snap_name,
128+ 'series': series or '16',
129+ 'blob': blob or self.generate_blob(snap_name),
130+ 'authority': authority or 'local-authority',
131+ 'private': private or False,
132+ 'publisher_id': publisher_id or 'some-publisher-id',
133+ 'stores': stores or ['some-store-id'],
134+ 'status': status or 'published',
135+ 'country_blacklist': country_blacklist or [],
136+ 'country_whitelist': country_whitelist or [],
137+ })
138+
139+ @staticmethod
140+ def generate_blob(snap_name):
141+ """Generate a payload as returned from snapident's fetch-blob endpoint.
142+
143+ The 'blob' in this example has been trimmed to only contain the
144+ values we actually use.
145+ """
146+ return {
147+ "origin": "developername",
148+ "last_updated": None,
149+ "package_name": snap_name,
150+ "screenshot_url": None,
151+ "developer_id": "46MtBuBZaWy3g8picgdg6YkrCQo84J46",
152+ "ratings_average": 0.0,
153+ "title": "snap title here",
154+ "support_url": "",
155+ "icon_url": None,
156+ "developer_name": "developername",
157+ "screenshot_urls": [],
158+ "description": "Description of the most simple snap",
159+ "price": 0.0,
160+ "translations": {},
161+ "prices": {},
162+ "publisher": "some publisher",
163+ "summary": "Summary of the most simple snap",
164+ }
165+
166+
167+class SnapIdent:
168+ """A namespace for functions that generate well-known snapident payloads.
169+
170+ The functions on this class all return JSON-serializable objects that
171+ can be set in the service mocks. The service mocks will ensure that they
172+ meet the output validation of the service in question.
173+ """
174+
175+ @staticmethod
176+ def NoSuchSnap():
177+ return SnapIdentBuilder().get_payload()
178+
179+ @staticmethod
180+ def SingleSnap(snap_name=None, snap_id=None, series=None, authority=None,
181+ private=None, publisher_id=None, stores=None, status=None,
182+ country_blacklist=None, country_whitelist=None):
183+ builder = SnapIdentBuilder()
184+ builder.add_snap(
185+ snap_name=snap_name,
186+ snap_id=snap_id,
187+ series=series,
188+ authority=authority,
189+ private=private,
190+ publisher_id=publisher_id,
191+ stores=stores,
192+ status=status,
193+ country_blacklist=country_blacklist,
194+ country_whitelist=country_whitelist,
195+ )
196+ return builder.get_payload()
197+
198+
199+class SnapFind:
200+ """A namespace for functions that generate well-known snapfind payloads.
201+
202+ The functions on this class all return JSON-serializable objects that
203+ can be set in the service mocks. The service mocks will ensure that they
204+ meet the output validation of the service in question.
205+ """
206+
207+ @staticmethod
208+ def SnapSectionList(snap_sections):
209+ return {
210+ 'sections': [
211+ {
212+ 'section_name': section_name,
213+ 'snaps': [
214+ {
215+ 'snap_id': snap_id,
216+ 'series': '16',
217+ 'featured': False,
218+ 'score': 0,
219+ } for snap_id in snap_ids
220+ ],
221+ } for section_name, snap_ids in snap_sections.items()]
222+ }
223+
224+
225+class SnapRevsBuilder:
226+
227+ def __init__(self):
228+ self.revisions = []
229+ self.channelmaps = []
230+
231+ def add_revision(self, snap_id, revision, **kwargs):
232+ self.revisions.append({
233+ 'snap_id': snap_id,
234+ 'revision': revision,
235+ # XXX snapd uses RFC3339, instead of 8601
236+ 'created_at': kwargs.get('created_at', '2017-01-01T00:00:00'),
237+ 'created_by': kwargs.get('created_by', 'developer_id_here'),
238+ 'architectures': kwargs.get('architectures', ['amd64']),
239+ 'binary_path': kwargs.get('binary_path', '/path/to.snap'),
240+ 'binary_filesize': kwargs.get('binary_filesize', 12345),
241+ 'binary_sha512': kwargs.get('binary_sha512', 'sha512-here'),
242+ 'binary_sha3_384': kwargs.get('binary_sha3_384', 'sha3384-here'),
243+ 'version': kwargs.get('version', '1.0'),
244+ 'confinement': kwargs.get('confinement', 'strict'),
245+ 'snap_yaml': kwargs.get('snap_yaml', ''),
246+ 'epoch': kwargs.get('epoch', 0),
247+ 'type': kwargs.get('type', 'app'),
248+ 'was_released': kwargs.get('was_released', False),
249+ 'is_released': kwargs.get('is_released', False),
250+ })
251+
252+ def get_payload(self):
253+ return {
254+ 'revisions': self.revisions,
255+ 'channelmaps': self.channelmaps,
256+ }
257+
258+
259+class SnapRevs():
260+
261+ @staticmethod
262+ def NoRevisions():
263+ return SnapRevsBuilder().get_payload()
264+
265+ @staticmethod
266+ def SingleRevision(snap_id, revision, **kwargs):
267+ builder = SnapRevsBuilder()
268+ builder.add_revision(snap_id, revision, **kwargs)
269+ # TODO: make the builder smarter about whether it should emit
270+ # channelmaps or not:
271+ return {'revisions': builder.get_payload()['revisions']}
272diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py
273new file mode 100644
274index 0000000..c31f16b
275--- /dev/null
276+++ b/snapstore_client/tests/test_webservices.py
277@@ -0,0 +1,439 @@
278+# Copyright 2017 Canonical Ltd.
279+
280+import base64
281+import binascii
282+import datetime
283+import hashlib
284+import json
285+import sys
286+import types
287+from urllib.parse import urljoin
288+
289+from acceptable._doubles import set_service_locations
290+import fixtures
291+from requests.exceptions import HTTPError
292+import responses
293+from snapstore_schemas.service_doubles import (
294+ snapfind,
295+ snapident,
296+ snaprevs,
297+)
298+from testtools import TestCase
299+
300+from snapstore_client import (
301+ config,
302+ webservices,
303+)
304+from snapstore_client.tests import factory
305+
306+if sys.version < '3.6':
307+ import sha3 # noqa
308+
309+
310+class NowFixture(fixtures.MonkeyPatch):
311+
312+ def __init__(self):
313+ self.now = datetime.datetime.now()
314+ super().__init__(
315+ 'datetime.datetime', types.SimpleNamespace(now=lambda: self.now))
316+
317+
318+class WebservicesTests(TestCase):
319+
320+ def setUp(self):
321+ super().setUp()
322+ service_locations = config.read_config()['services']
323+ set_service_locations(service_locations)
324+
325+ def test_get_snap_id_for_name_no_match(self):
326+ self.useFixture(snapident.filter_snaps_1_0(
327+ factory.SnapIdent.NoSuchSnap()))
328+
329+ self.assertIsNone(webservices.get_snap_id_for_name('mysnap', '16'))
330+
331+ def test_get_snap_id_for_name_match(self):
332+ snap_name = 'mysnap'
333+ snap_id = factory.generate_snap_id()
334+ series = '16'
335+ self.useFixture(snapident.filter_snaps_1_0(
336+ factory.SnapIdent.SingleSnap(snap_name, snap_id, series=series)))
337+
338+ self.assertEqual(
339+ snap_id, webservices.get_snap_id_for_name(snap_name, series))
340+
341+ def test_register_snap_name_and_blob_success(self):
342+ logger = self.useFixture(fixtures.FakeLogger())
343+ builder = factory.SnapIdentBuilder()
344+ builder.add_snap()
345+ snap = builder.get_payload()['snaps'][0]
346+ snapident_fixture = self.useFixture(
347+ snapident.update_snaps_1_0({'ok': True}))
348+
349+ self.assertFalse(webservices.register_snap_name_and_blob(
350+ snap['snap_id'], snap['snap_name'], snap['series'], snap['blob'],
351+ authority=snap['authority']))
352+ snapident_request = snapident_fixture.calls[0].request
353+ self.assertEqual({
354+ 'snaps': [
355+ {
356+ 'snap_id': snap['snap_id'],
357+ 'private': False,
358+ 'publisher_id': snap['blob']['developer_id'],
359+ 'snap_name': snap['snap_name'],
360+ 'series': snap['series'],
361+ 'blob': snap['blob'],
362+ 'authority': snap['authority'],
363+ 'status': 'published',
364+ 'stores': ['ubuntu'],
365+ },
366+ ],
367+ }, json.loads(snapident_request.body.decode()))
368+ self.assertNotIn('Failed to register snap:', logger.output)
369+
370+ @responses.activate
371+ def test_register_snap_name_and_blob_error(self):
372+ logger = self.useFixture(fixtures.FakeLogger())
373+ builder = factory.SnapIdentBuilder()
374+ builder.add_snap()
375+ snap = builder.get_payload()['snaps'][0]
376+ update_snaps_url = urljoin(
377+ config.read_config()['services']['snapident'], '/snaps/update')
378+ responses.add(
379+ 'POST', update_snaps_url, status=400,
380+ json=factory.APIError.single('Something went wrong').to_dict())
381+
382+ self.assertTrue(webservices.register_snap_name_and_blob(
383+ snap['snap_id'], snap['snap_name'], snap['series'], snap['blob'],
384+ authority=snap['authority']))
385+ self.assertEqual(
386+ 'Failed to register snap:\nSomething went wrong\n', logger.output)
387+
388+ def test_release_revision_success(self):
389+ logger = self.useFixture(fixtures.FakeLogger())
390+ snap_name = 'mysnap'
391+ snap_id = factory.generate_snap_id()
392+ series = '16'
393+ channel = [None, 'edge', None]
394+ arches = ['amd64', 'armhf']
395+ revision = 1
396+ snaprevs_fixture = self.useFixture(
397+ snaprevs.update_channelmaps_1_0({'success': True}))
398+
399+ webservices.release_revision(
400+ snap_id, snap_name, series, channel, arches, revision=revision)
401+ snaprevs_request = snaprevs_fixture.calls[0].request
402+ self.assertEqual({
403+ 'developer_id': 'wgrant',
404+ 'release_requests': [
405+ {
406+ 'snap_id': snap_id,
407+ 'channel': channel,
408+ 'architecture': arch,
409+ 'series': series,
410+ 'revision': revision,
411+ } for arch in arches
412+ ],
413+ }, json.loads(snaprevs_request.body.decode()))
414+ self.assertNotIn('Failed to release revision:', logger.output)
415+
416+ @responses.activate
417+ def test_release_revision_error(self):
418+ logger = self.useFixture(fixtures.FakeLogger())
419+ update_channelmaps_url = urljoin(
420+ config.read_config()['services']['snaprevs'],
421+ '/channelmaps/update')
422+ responses.add(
423+ 'POST', update_channelmaps_url, status=400,
424+ json=factory.APIError.single('Something went wrong').to_dict())
425+
426+ webservices.release_revision(
427+ factory.generate_snap_id(), 'mysnap', '16', [None, 'edge', None],
428+ ['amd64', 'armhf'], revision=1)
429+ self.assertEqual(
430+ 'Failed to release revision:\nSomething went wrong\n',
431+ logger.output)
432+
433+ def test_get_latest_revision_for_snap_id_no_revisions(self):
434+ snap_id = factory.generate_snap_id()
435+ self.useFixture(
436+ snaprevs.fetch_revisions_1_0(factory.SnapRevs.NoRevisions()))
437+
438+ self.assertIsNone(webservices.get_latest_revision_for_snap_id(snap_id))
439+
440+ def test_get_latest_revision_for_snap_id_revisions(self):
441+ snap_id = factory.generate_snap_id()
442+ builder = factory.SnapRevsBuilder()
443+ builder.add_revision(snap_id, 1)
444+ builder.add_revision(snap_id, 2)
445+ self.useFixture(
446+ snaprevs.fetch_revisions_1_0(
447+ {'revisions': builder.get_payload()['revisions']}))
448+
449+ self.assertEqual(
450+ 2, webservices.get_latest_revision_for_snap_id(snap_id))
451+
452+ def test_create_revision_success(self):
453+ logger = self.useFixture(fixtures.FakeLogger())
454+ now_fixture = self.useFixture(NowFixture())
455+ snap_id = factory.generate_snap_id()
456+ snaprevs_fixture = self.useFixture(
457+ snaprevs.create_revisions_1_0(
458+ {'num_revisions_created': 1}, output_status=201))
459+
460+ self.assertTrue(webservices.create_revision(
461+ snap_id, 1, ['amd64'], '/path/to/blob.snap', 4096,
462+ hashlib.sha512(b'blob').hexdigest(),
463+ hashlib.sha3_384(b'blob').hexdigest(),
464+ '1.0', 'strict', 'name: blob\n'))
465+ snaprevs_request = snaprevs_fixture.calls[0].request
466+ self.assertEqual([
467+ {
468+ 'snap_id': snap_id,
469+ 'revision': 1,
470+ 'created_at': now_fixture.now.isoformat(),
471+ 'created_by': 'TODO',
472+ 'architectures': ['amd64'],
473+ 'binary_path': '/path/to/blob.snap',
474+ 'binary_filesize': 4096,
475+ 'binary_sha512': hashlib.sha512(b'blob').hexdigest(),
476+ 'binary_sha3_384': hashlib.sha3_384(b'blob').hexdigest(),
477+ 'version': '1.0',
478+ 'confinement': 'strict',
479+ 'snap_yaml': 'name: blob\n',
480+ 'epoch': 0,
481+ 'type': 'app',
482+ },
483+ ], json.loads(snaprevs_request.body.decode()))
484+ self.assertNotIn('Failed to create revision:', logger.output)
485+
486+ @responses.activate
487+ def test_create_revision_error(self):
488+ logger = self.useFixture(fixtures.FakeLogger())
489+ snap_id = factory.generate_snap_id()
490+ create_revisions_url = urljoin(
491+ config.read_config()['services']['snaprevs'], '/revisions/create')
492+ responses.add(
493+ 'POST', create_revisions_url, status=400,
494+ json=factory.APIError.single('Something went wrong').to_dict())
495+
496+ self.assertFalse(webservices.create_revision(
497+ snap_id, 1, ['amd64'], '/path/to/blob.snap', 4096,
498+ hashlib.sha512(b'blob').hexdigest(),
499+ hashlib.sha3_384(b'blob').hexdigest(),
500+ '1.0', 'strict', 'name: blob\n'))
501+ self.assertEqual(
502+ 'Failed to create revision:\nSomething went wrong\n',
503+ logger.output)
504+
505+ def test_create_snapsections_success(self):
506+ logger = self.useFixture(fixtures.FakeLogger())
507+ snapfind_fixture = self.useFixture(
508+ snapfind.update_snap_section_1_0({'ok': True}))
509+
510+ snap_sections = factory.SnapFind.SnapSectionList({
511+ 'section1': [factory.generate_snap_id()],
512+ 'section2': [factory.generate_snap_id()],
513+ })
514+ webservices.create_snapsections(snap_sections)
515+ snapfind_request = snapfind_fixture.calls[0].request
516+ self.assertEqual(
517+ snap_sections, json.loads(snapfind_request.body.decode()))
518+ self.assertEqual(
519+ 'Updating sections and snapsections...\nDone.\n', logger.output)
520+
521+ @responses.activate
522+ def test_create_snapsections_error(self):
523+ logger = self.useFixture(fixtures.FakeLogger())
524+ update_snap_section_url = urljoin(
525+ config.read_config()['services']['snapfind'], '/sections/snaps')
526+ responses.add(
527+ 'POST', update_snap_section_url, status=400,
528+ json=factory.APIError.single('Something went wrong').to_dict())
529+
530+ webservices.create_snapsections({})
531+ self.assertEqual(
532+ 'Updating sections and snapsections...\n'
533+ 'Failed to update sections:\nSomething went wrong\n',
534+ logger.output)
535+
536+ @responses.activate
537+ def test_get_assertion_success(self):
538+ assertions_root = config.read_config()['services']['assertions']
539+ get_assertions_url = urljoin(
540+ assertions_root, 'assertions/snap-revision/dummy')
541+ responses.add(
542+ 'GET', get_assertions_url, status=200, body='Dummy assertion\n')
543+
544+ self.assertEqual(
545+ b'Dummy assertion\n',
546+ webservices.get_assertion(
547+ assertions_root, 'snap-revision', ['dummy']))
548+
549+ @responses.activate
550+ def test_get_assertion_error(self):
551+ assertions_root = config.read_config()['services']['assertions']
552+ get_assertions_url = urljoin(
553+ assertions_root, 'assertions/snap-revision/dummy')
554+ responses.add('GET', get_assertions_url, status=400)
555+
556+ self.assertIsNone(webservices.get_assertion(
557+ assertions_root, 'snap-revision', ['dummy']))
558+
559+ @responses.activate
560+ def test_create_or_update_assertions_no_snap_declaration(self):
561+ now_fixture = self.useFixture(NowFixture())
562+ assertions_root = config.read_config()['services']['assertions']
563+ authority = config.read_config()['assertions']['authority']
564+ snap_id = factory.generate_snap_id()
565+ binary_sha3_384 = hashlib.sha3_384(b'blob').hexdigest()
566+ snap_sha3_384 = base64.urlsafe_b64encode(
567+ binascii.a2b_hex(binary_sha3_384)).decode().rstrip('=')
568+ get_assertions_url = urljoin(
569+ assertions_root,
570+ 'assertions/snap-declaration/16/{}'.format(snap_id))
571+ sign_assertions_url = urljoin(assertions_root, 'sign')
572+ save_assertions_url = urljoin(assertions_root, 'assertions')
573+ responses.add('GET', get_assertions_url, status=404)
574+ signed_assertions_iter = iter([
575+ (200, {}, 'Dummy snap-declaration assertion\n'),
576+ (200, {}, 'Dummy snap-revision assertion\n'),
577+ ])
578+ responses.add_callback(
579+ 'POST', sign_assertions_url,
580+ callback=lambda _: next(signed_assertions_iter))
581+ responses.add('POST', save_assertions_url, status=201)
582+
583+ webservices.create_or_update_assertions(
584+ snap_id, 'mysnap', '16', 1, binary_sha3_384, 4096)
585+ (_, declaration_sign_request, declaration_save_request,
586+ revision_sign_request, revision_save_request) = [
587+ call.request for call in responses.calls]
588+ self.assertEqual({
589+ 'key-id': config.read_config()['assertions']['signing_key_id'],
590+ 'headers': {
591+ 'type': 'snap-declaration',
592+ 'revision': '0',
593+ 'authority-id': authority,
594+ 'publisher-id': authority,
595+ 'series': '16',
596+ 'snap-id': snap_id,
597+ 'snap-name': 'mysnap',
598+ 'timestamp': now_fixture.now.isoformat() + 'Z',
599+ },
600+ }, json.loads(declaration_sign_request.body.decode()))
601+ self.assertEqual(
602+ 'Dummy snap-declaration assertion\n',
603+ declaration_save_request.body)
604+ self.assertEqual({
605+ 'key-id': config.read_config()['assertions']['signing_key_id'],
606+ 'headers': {
607+ 'type': 'snap-revision',
608+ 'authority-id': authority,
609+ 'developer-id': authority,
610+ 'snap-sha3-384': snap_sha3_384,
611+ 'snap-id': snap_id,
612+ 'snap-size': '4096',
613+ 'snap-revision': '1',
614+ 'timestamp': now_fixture.now.isoformat() + 'Z',
615+ },
616+ }, json.loads(revision_sign_request.body.decode()))
617+ self.assertEqual(
618+ 'Dummy snap-revision assertion\n', revision_save_request.body)
619+
620+ @responses.activate
621+ def test_create_or_update_assertions_with_snap_declaration(self):
622+ now_fixture = self.useFixture(NowFixture())
623+ assertions_root = config.read_config()['services']['assertions']
624+ authority = config.read_config()['assertions']['authority']
625+ snap_id = factory.generate_snap_id()
626+ binary_sha3_384 = hashlib.sha3_384(b'blob').hexdigest()
627+ snap_sha3_384 = base64.urlsafe_b64encode(
628+ binascii.a2b_hex(binary_sha3_384)).decode().rstrip('=')
629+ get_assertions_url = urljoin(
630+ assertions_root,
631+ 'assertions/snap-declaration/16/{}'.format(snap_id))
632+ sign_assertions_url = urljoin(assertions_root, 'sign')
633+ save_assertions_url = urljoin(assertions_root, 'assertions')
634+ responses.add(
635+ 'GET', get_assertions_url, status=200,
636+ body='Dummy snap-declaration assertion\n')
637+ responses.add(
638+ 'POST', sign_assertions_url, status=200,
639+ body='Dummy snap-revision assertion\n')
640+ responses.add('POST', save_assertions_url, status=201)
641+
642+ webservices.create_or_update_assertions(
643+ snap_id, 'mysnap', '16', 1, binary_sha3_384, 4096)
644+ _, sign_request, save_request = [
645+ call.request for call in responses.calls]
646+ self.assertEqual({
647+ 'key-id': config.read_config()['assertions']['signing_key_id'],
648+ 'headers': {
649+ 'type': 'snap-revision',
650+ 'authority-id': authority,
651+ 'developer-id': authority,
652+ 'snap-sha3-384': snap_sha3_384,
653+ 'snap-id': snap_id,
654+ 'snap-size': '4096',
655+ 'snap-revision': '1',
656+ 'timestamp': now_fixture.now.isoformat() + 'Z',
657+ },
658+ }, json.loads(sign_request.body.decode()))
659+
660+ @responses.activate
661+ def test_save_assertion_success(self):
662+ assertions_root = config.read_config()['services']['assertions']
663+ save_assertions_url = urljoin(assertions_root, 'assertions')
664+ responses.add('POST', save_assertions_url, status=201)
665+
666+ webservices.save_assertion('Dummy assertion\n')
667+ request = responses.calls[0].request
668+ self.assertEqual(
669+ 'application/x.ubuntu.assertion', request.headers['Content-Type'])
670+ self.assertEqual('Dummy assertion\n', request.body)
671+
672+ @responses.activate
673+ def test_save_assertion_error(self):
674+ logger = self.useFixture(fixtures.FakeLogger())
675+ assertions_root = config.read_config()['services']['assertions']
676+ save_assertions_url = urljoin(assertions_root, 'assertions')
677+ responses.add(
678+ 'POST', save_assertions_url, status=400,
679+ json=factory.APIError.single('Something went wrong').to_dict())
680+
681+ self.assertRaises(
682+ HTTPError, webservices.save_assertion, 'Dummy assertion\n')
683+ self.assertEqual(
684+ 'Failed to save assertion:\nSomething went wrong\n',
685+ logger.output)
686+
687+ @responses.activate
688+ def test_sign_assertion_success(self):
689+ assertions_root = config.read_config()['services']['assertions']
690+ sign_assertions_url = urljoin(assertions_root, 'sign')
691+ responses.add(
692+ 'POST', sign_assertions_url, status=200, body='Dummy assertion\n')
693+
694+ self.assertEqual(
695+ 'Dummy assertion\n', webservices.sign_assertion({'revision': 1}))
696+ request = responses.calls[0].request
697+ self.assertEqual('application/json', request.headers['Content-Type'])
698+ self.assertEqual({
699+ 'key-id': config.read_config()['assertions']['signing_key_id'],
700+ 'headers': {'revision': 1},
701+ }, json.loads(request.body.decode()))
702+
703+ @responses.activate
704+ def test_sign_assertion_error(self):
705+ logger = self.useFixture(fixtures.FakeLogger())
706+ assertions_root = config.read_config()['services']['assertions']
707+ sign_assertions_url = urljoin(assertions_root, 'sign')
708+ responses.add(
709+ 'POST', sign_assertions_url, status=400,
710+ json=factory.APIError.single('Something went wrong').to_dict())
711+
712+ self.assertRaises(
713+ HTTPError, webservices.sign_assertion, {'revision': 1})
714+ self.assertEqual(
715+ 'Failed to sign assertion:\nSomething went wrong\n',
716+ logger.output)
717diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py
718index 4c5e234..43e122c 100644
719--- a/snapstore_client/webservices.py
720+++ b/snapstore_client/webservices.py
721@@ -126,7 +126,7 @@ def create_revision(snap_id, revision, architectures, binary_path, filesize,
722 sha512, sha3_384, version, confinement, snap_yaml):
723 """Create a snap revision with snaprevs.
724
725- Returns True on succcess, False otherwise.
726+ Returns True on success, False otherwise.
727 """
728 snaprevs_root = config.read_config()['services']['snaprevs']
729 # TODO: I suspect this is supposed to be UTC time, not local time?
730@@ -172,7 +172,7 @@ def create_snapsections(payload):
731 if response.ok:
732 logger.info('Done.')
733 else:
734- logger.error(response.text)
735+ _print_error_message('update sections', response)
736
737
738 def get_assertion(root, type_, key):

Subscribers

People subscribed via source and target branches

to all changes: