Merge ~cjwatson/snapstore-client:webservices-tests into snapstore-client:master
- Git
- lp:~cjwatson/snapstore-client
- webservices-tests
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matt Goodall (community) | Approve | ||
Review via email: mp+326159@code.launchpad.net |
Commit message
Add webservices tests
Description of the change
To post a comment you must log in.
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
1 | diff --git a/requirements-dev.txt b/requirements-dev.txt |
2 | index 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 |
14 | diff --git a/snapstore_client/tests/factory.py b/snapstore_client/tests/factory.py |
15 | new file mode 100644 |
16 | index 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']} |
272 | diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py |
273 | new file mode 100644 |
274 | index 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) |
717 | diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py |
718 | index 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): |
looks good. some minor comments, mostly optional and/or pedantic :)