Merge lp:~cjwatson/launchpad/responses into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18680
Proposed branch: lp:~cjwatson/launchpad/responses
Merge into: lp:launchpad
Diff against target: 2174 lines (+590/-720)
8 files modified
constraints.txt (+2/-1)
lib/lp/bugs/externalbugtracker/tests/test_github.py (+164/-169)
lib/lp/code/model/tests/test_githosting.py (+63/-74)
lib/lp/services/webhooks/tests/test_job.py (+24/-35)
lib/lp/snappy/browser/tests/test_snap.py (+30/-48)
lib/lp/snappy/tests/test_snap.py (+8/-16)
lib/lp/snappy/tests/test_snapstoreclient.py (+298/-376)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/responses
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+347193@code.launchpad.net

Commit message

Port from httmock to responses.

Description of the change

For a long time I'd thought these two were much of a muchness and it didn't really matter which we used. However, when porting the external bug tracker code to requests, I found that httmock has a significant flaw: it doesn't support hooks (http://docs.python-requests.org/en/master/user/advanced/#event-hooks). While that could be patched in, it happens because httmock patches itself in one level higher than responses (at the session level rather than at the adapter level), and so has to duplicate a certain amount of the internal logic of requests. As such it seems to me that responses has a better design, and it's worth switching to it. I thought it best to just do this in one shot to keep things tidy.

This isn't particularly mechanical because the two libraries have quite different APIs, but it's test-only and introduces no functional changes.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'constraints.txt'
2--- constraints.txt 2018-05-28 10:16:28 +0000
3+++ constraints.txt 2018-05-31 10:48:11 +0000
4@@ -239,6 +239,7 @@
5 Chameleon==2.11
6 chardet==3.0.4
7 constantly==15.1.0
8+cookies==2.2.1
9 cryptography==2.1.4
10 cssselect==0.9.1
11 cssutils==0.9.10
12@@ -258,7 +259,6 @@
13 FormEncode==1.2.4
14 grokcore.component==1.6
15 html5browser==0.0.9
16-httmock==1.2.3
17 httplib2==0.8
18 hyperlink==18.0.0
19 idna==2.6
20@@ -337,6 +337,7 @@
21 rabbitfixture==0.3.6
22 requests==2.7.0
23 requests-toolbelt==0.6.2
24+responses==0.9.0
25 scandir==1.7
26 service-identity==17.0.0
27 setproctitle==1.1.7
28
29=== modified file 'lib/lp/bugs/externalbugtracker/tests/test_github.py'
30--- lib/lp/bugs/externalbugtracker/tests/test_github.py 2016-12-15 06:53:13 +0000
31+++ lib/lp/bugs/externalbugtracker/tests/test_github.py 2018-05-31 10:48:11 +0000
32@@ -1,4 +1,4 @@
33-# Copyright 2016 Canonical Ltd. This software is licensed under the
34+# Copyright 2016-2018 Canonical Ltd. This software is licensed under the
35 # GNU Affero General Public License version 3 (see the file LICENSE).
36
37 """Tests for the GitHub Issues BugTracker."""
38@@ -9,16 +9,22 @@
39
40 from datetime import datetime
41 import json
42-from urlparse import (
43+
44+import pytz
45+import responses
46+from six.moves.urllib_parse import (
47 parse_qs,
48+ urlsplit,
49 urlunsplit,
50 )
51-
52-from httmock import (
53- HTTMock,
54- urlmatch,
55+from testtools.matchers import (
56+ Contains,
57+ ContainsDict,
58+ Equals,
59+ MatchesListwise,
60+ MatchesStructure,
61+ Not,
62 )
63-import pytz
64 import transaction
65 from zope.component import getUtility
66
67@@ -49,6 +55,14 @@
68 )
69
70
71+def _add_rate_limit_response(host, limit=5000, remaining=4000,
72+ reset=1000000000):
73+ limits = {"limit": limit, "remaining": remaining, "reset": reset}
74+ responses.add(
75+ "GET", "https://%s/rate_limit" % host,
76+ json={"resources": {"core": limits}})
77+
78+
79 class TestGitHubRateLimit(TestCase):
80
81 layer = ZopelessLayer
82@@ -58,77 +72,61 @@
83 self.rate_limit = getUtility(IGitHubRateLimit)
84 self.addCleanup(self.rate_limit.clearCache)
85
86- @urlmatch(path=r"^/rate_limit$")
87- def _rate_limit_handler(self, url, request):
88- self.rate_limit_request = request
89- self.rate_limit_headers = request.headers
90- return {
91- "status_code": 200,
92- "content": {"resources": {"core": self.initial_rate_limit}},
93- }
94-
95- @urlmatch(path=r"^/$")
96- def _target_handler(self, url, request):
97- self.target_request = request
98- return {"status_code": 200, "content": b"test"}
99-
100+ @responses.activate
101 def test_makeRequest_no_token(self):
102- self.initial_rate_limit = {
103- "limit": 60, "remaining": 50, "reset": 1000000000}
104- with HTTMock(self._rate_limit_handler, self._target_handler):
105- response = self.rate_limit.makeRequest(
106- "GET", "http://example.org/")
107- self.assertNotIn("Authorization", self.rate_limit_headers)
108+ _add_rate_limit_response("example.org", limit=60, remaining=50)
109+ responses.add("GET", "http://example.org/", body="test")
110+ response = self.rate_limit.makeRequest("GET", "http://example.org/")
111+ self.assertThat(responses.calls[0].request, MatchesStructure(
112+ path_url=Equals("/rate_limit"),
113+ headers=Not(Contains("Authorization"))))
114 self.assertEqual(b"test", response.content)
115 limit = self.rate_limit._limits[("example.org", None)]
116 self.assertEqual(49, limit["remaining"])
117 self.assertEqual(1000000000, limit["reset"])
118
119 limit["remaining"] = 0
120- self.rate_limit_request = None
121- with HTTMock(self._rate_limit_handler, self._target_handler):
122- self.assertRaisesWithContent(
123- GitHubExceededRateLimit,
124- "Rate limit for example.org exceeded "
125- "(resets at Sun Sep 9 07:16:40 2001)",
126- self.rate_limit.makeRequest,
127- "GET", "http://example.org/")
128- self.assertIsNone(self.rate_limit_request)
129+ responses.reset()
130+ self.assertRaisesWithContent(
131+ GitHubExceededRateLimit,
132+ "Rate limit for example.org exceeded "
133+ "(resets at Sun Sep 9 07:16:40 2001)",
134+ self.rate_limit.makeRequest,
135+ "GET", "http://example.org/")
136+ self.assertEqual(0, len(responses.calls))
137 self.assertEqual(0, limit["remaining"])
138
139+ @responses.activate
140 def test_makeRequest_check_token(self):
141- self.initial_rate_limit = {
142- "limit": 5000, "remaining": 4000, "reset": 1000000000}
143- with HTTMock(self._rate_limit_handler, self._target_handler):
144- response = self.rate_limit.makeRequest(
145- "GET", "http://example.org/", token="abc")
146- self.assertEqual("token abc", self.rate_limit_headers["Authorization"])
147+ _add_rate_limit_response("example.org")
148+ responses.add("GET", "http://example.org/", body="test")
149+ response = self.rate_limit.makeRequest(
150+ "GET", "http://example.org/", token="abc")
151+ self.assertThat(responses.calls[0].request, MatchesStructure(
152+ path_url=Equals("/rate_limit"),
153+ headers=ContainsDict({"Authorization": Equals("token abc")})))
154 self.assertEqual(b"test", response.content)
155 limit = self.rate_limit._limits[("example.org", "abc")]
156 self.assertEqual(3999, limit["remaining"])
157 self.assertEqual(1000000000, limit["reset"])
158
159 limit["remaining"] = 0
160- self.rate_limit_request = None
161- with HTTMock(self._rate_limit_handler, self._target_handler):
162- self.assertRaisesWithContent(
163- GitHubExceededRateLimit,
164- "Rate limit for example.org exceeded "
165- "(resets at Sun Sep 9 07:16:40 2001)",
166- self.rate_limit.makeRequest,
167- "GET", "http://example.org/", token="abc")
168- self.assertIsNone(self.rate_limit_request)
169+ responses.reset()
170+ self.assertRaisesWithContent(
171+ GitHubExceededRateLimit,
172+ "Rate limit for example.org exceeded "
173+ "(resets at Sun Sep 9 07:16:40 2001)",
174+ self.rate_limit.makeRequest,
175+ "GET", "http://example.org/", token="abc")
176+ self.assertEqual(0, len(responses.calls))
177 self.assertEqual(0, limit["remaining"])
178
179+ @responses.activate
180 def test_makeRequest_check_503(self):
181- @urlmatch(path=r"^/rate_limit$")
182- def rate_limit_handler(url, request):
183- return {"status_code": 503}
184-
185- with HTTMock(rate_limit_handler):
186- self.assertRaises(
187- BugTrackerConnectError, self.rate_limit.makeRequest,
188- "GET", "http://example.org/")
189+ responses.add("GET", "https://example.org/rate_limit", status=503)
190+ self.assertRaises(
191+ BugTrackerConnectError, self.rate_limit.makeRequest,
192+ "GET", "http://example.org/")
193
194
195 class TestGitHub(TestCase):
196@@ -156,105 +154,108 @@
197 self.assertRaises(
198 BadGitHubURL, GitHub, "https://github.com/user/repository")
199
200- @urlmatch(path=r"^/rate_limit$")
201- def _rate_limit_handler(self, url, request):
202- self.rate_limit_request = request
203- rate_limit = {"limit": 5000, "remaining": 4000, "reset": 1000000000}
204- return {
205- "status_code": 200,
206- "content": {"resources": {"core": rate_limit}},
207- }
208-
209+ @responses.activate
210 def test__getPage_authenticated(self):
211- @urlmatch(path=r".*/test$")
212- def handler(url, request):
213- self.request = request
214- return {"status_code": 200, "content": json.dumps("success")}
215-
216+ _add_rate_limit_response("api.github.com")
217+ responses.add(
218+ "GET", "https://api.github.com/repos/user/repository/test",
219+ json="success")
220 self.pushConfig(
221 "checkwatches.credentials", **{"api.github.com.token": "sosekrit"})
222 tracker = GitHub("https://github.com/user/repository/issues")
223- with HTTMock(self._rate_limit_handler, handler):
224- self.assertEqual("success", tracker._getPage("test").json())
225- self.assertEqual(
226- "https://api.github.com/repos/user/repository/test",
227- self.request.url)
228- self.assertEqual(
229- "token sosekrit", self.request.headers["Authorization"])
230- self.assertEqual(
231- "token sosekrit", self.rate_limit_request.headers["Authorization"])
232+ self.assertEqual("success", tracker._getPage("test").json())
233+ requests = [call.request for call in responses.calls]
234+ self.assertThat(requests, MatchesListwise([
235+ MatchesStructure(
236+ path_url=Equals("/rate_limit"),
237+ headers=ContainsDict({
238+ "Authorization": Equals("token sosekrit"),
239+ })),
240+ MatchesStructure(
241+ path_url=Equals("/repos/user/repository/test"),
242+ headers=ContainsDict({
243+ "Authorization": Equals("token sosekrit"),
244+ })),
245+ ]))
246
247+ @responses.activate
248 def test__getPage_unauthenticated(self):
249- @urlmatch(path=r".*/test$")
250- def handler(url, request):
251- self.request = request
252- return {"status_code": 200, "content": json.dumps("success")}
253-
254+ _add_rate_limit_response("api.github.com")
255+ responses.add(
256+ "GET", "https://api.github.com/repos/user/repository/test",
257+ json="success")
258 tracker = GitHub("https://github.com/user/repository/issues")
259- with HTTMock(self._rate_limit_handler, handler):
260- self.assertEqual("success", tracker._getPage("test").json())
261- self.assertEqual(
262- "https://api.github.com/repos/user/repository/test",
263- self.request.url)
264- self.assertNotIn("Authorization", self.request.headers)
265- self.assertNotIn("Authorization", self.rate_limit_request.headers)
266+ self.assertEqual("success", tracker._getPage("test").json())
267+ requests = [call.request for call in responses.calls]
268+ self.assertThat(requests, MatchesListwise([
269+ MatchesStructure(
270+ path_url=Equals("/rate_limit"),
271+ headers=Not(Contains("Authorization"))),
272+ MatchesStructure(
273+ path_url=Equals("/repos/user/repository/test"),
274+ headers=Not(Contains("Authorization"))),
275+ ]))
276
277+ @responses.activate
278 def test_getRemoteBug(self):
279- @urlmatch(path=r".*/issues/1$")
280- def handler(url, request):
281- self.request = request
282- return {"status_code": 200, "content": self.sample_bugs[0]}
283-
284+ _add_rate_limit_response("api.github.com")
285+ responses.add(
286+ "GET", "https://api.github.com/repos/user/repository/issues/1",
287+ json=self.sample_bugs[0])
288 tracker = GitHub("https://github.com/user/repository/issues")
289- with HTTMock(self._rate_limit_handler, handler):
290- self.assertEqual(
291- (1, self.sample_bugs[0]), tracker.getRemoteBug("1"))
292+ self.assertEqual((1, self.sample_bugs[0]), tracker.getRemoteBug("1"))
293 self.assertEqual(
294 "https://api.github.com/repos/user/repository/issues/1",
295- self.request.url)
296-
297- @urlmatch(path=r".*/issues$")
298- def _issues_handler(self, url, request):
299- self.issues_request = request
300- return {"status_code": 200, "content": json.dumps(self.sample_bugs)}
301-
302+ responses.calls[-1].request.url)
303+
304+ def _addIssuesResponse(self):
305+ responses.add(
306+ "GET", "https://api.github.com/repos/user/repository/issues",
307+ json=self.sample_bugs)
308+
309+ @responses.activate
310 def test_getRemoteBugBatch(self):
311+ _add_rate_limit_response("api.github.com")
312+ self._addIssuesResponse()
313 tracker = GitHub("https://github.com/user/repository/issues")
314- with HTTMock(self._rate_limit_handler, self._issues_handler):
315- self.assertEqual(
316- {bug["id"]: bug for bug in self.sample_bugs[:2]},
317- tracker.getRemoteBugBatch(["1", "2"]))
318+ self.assertEqual(
319+ {bug["id"]: bug for bug in self.sample_bugs[:2]},
320+ tracker.getRemoteBugBatch(["1", "2"]))
321 self.assertEqual(
322 "https://api.github.com/repos/user/repository/issues?state=all",
323- self.issues_request.url)
324+ responses.calls[-1].request.url)
325
326+ @responses.activate
327 def test_getRemoteBugBatch_last_accessed(self):
328+ _add_rate_limit_response("api.github.com")
329+ self._addIssuesResponse()
330 tracker = GitHub("https://github.com/user/repository/issues")
331 since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)
332- with HTTMock(self._rate_limit_handler, self._issues_handler):
333- self.assertEqual(
334- {bug["id"]: bug for bug in self.sample_bugs[:2]},
335- tracker.getRemoteBugBatch(["1", "2"], last_accessed=since))
336+ self.assertEqual(
337+ {bug["id"]: bug for bug in self.sample_bugs[:2]},
338+ tracker.getRemoteBugBatch(["1", "2"], last_accessed=since))
339 self.assertEqual(
340 "https://api.github.com/repos/user/repository/issues?"
341 "state=all&since=2015-01-01T12%3A00%3A00Z",
342- self.issues_request.url)
343+ responses.calls[-1].request.url)
344
345+ @responses.activate
346 def test_getRemoteBugBatch_caching(self):
347+ _add_rate_limit_response("api.github.com")
348+ self._addIssuesResponse()
349 tracker = GitHub("https://github.com/user/repository/issues")
350- with HTTMock(self._rate_limit_handler, self._issues_handler):
351- tracker.initializeRemoteBugDB(
352- [str(bug["id"]) for bug in self.sample_bugs])
353- self.issues_request = None
354- self.assertEqual(
355- {bug["id"]: bug for bug in self.sample_bugs[:2]},
356- tracker.getRemoteBugBatch(["1", "2"]))
357- self.assertIsNone(self.issues_request)
358+ tracker.initializeRemoteBugDB(
359+ [str(bug["id"]) for bug in self.sample_bugs])
360+ responses.reset()
361+ self.assertEqual(
362+ {bug["id"]: bug for bug in self.sample_bugs[:2]},
363+ tracker.getRemoteBugBatch(["1", "2"]))
364+ self.assertEqual(0, len(responses.calls))
365
366+ @responses.activate
367 def test_getRemoteBugBatch_pagination(self):
368- @urlmatch(path=r".*/issues")
369- def handler(url, request):
370- self.issues_requests.append(request)
371+ def issues_callback(request):
372+ url = urlsplit(request.url)
373 base_url = urlunsplit(list(url[:3]) + ["", ""])
374 page = int(parse_qs(url.query).get("page", ["1"])[0])
375 links = []
376@@ -266,27 +267,29 @@
377 links.append('<%s?page=%d>; rel="prev"' % (base_url, page - 1))
378 start = (page - 1) * 2
379 end = page * 2
380- return {
381- "status_code": 200,
382- "headers": {"Link": ", ".join(links)},
383- "content": json.dumps(self.sample_bugs[start:end]),
384- }
385+ return (
386+ 200, {"Link": ", ".join(links)},
387+ json.dumps(self.sample_bugs[start:end]))
388
389- self.issues_requests = []
390+ _add_rate_limit_response("api.github.com")
391+ responses.add_callback(
392+ "GET", "https://api.github.com/repos/user/repository/issues",
393+ callback=issues_callback, content_type="application/json")
394 tracker = GitHub("https://github.com/user/repository/issues")
395- with HTTMock(self._rate_limit_handler, handler):
396- self.assertEqual(
397- {bug["id"]: bug for bug in self.sample_bugs},
398- tracker.getRemoteBugBatch(
399- [str(bug["id"]) for bug in self.sample_bugs]))
400+ self.assertEqual(
401+ {bug["id"]: bug for bug in self.sample_bugs},
402+ tracker.getRemoteBugBatch(
403+ [str(bug["id"]) for bug in self.sample_bugs]))
404 expected_urls = [
405+ "https://api.github.com/rate_limit",
406 "https://api.github.com/repos/user/repository/issues?state=all",
407 "https://api.github.com/repos/user/repository/issues?page=2",
408 "https://api.github.com/repos/user/repository/issues?page=3",
409 ]
410 self.assertEqual(
411- expected_urls, [request.url for request in self.issues_requests])
412+ expected_urls, [call.request.url for call in responses.calls])
413
414+ @responses.activate
415 def test_status_open(self):
416 self.sample_bugs = [
417 {"id": 1, "state": "open", "labels": []},
418@@ -294,9 +297,10 @@
419 {"id": 2, "state": "open",
420 "labels": [{"name": "feature"}, {"name": "closed"}]},
421 ]
422+ _add_rate_limit_response("api.github.com")
423+ self._addIssuesResponse()
424 tracker = GitHub("https://github.com/user/repository/issues")
425- with HTTMock(self._rate_limit_handler, self._issues_handler):
426- tracker.initializeRemoteBugDB(["1", "2"])
427+ tracker.initializeRemoteBugDB(["1", "2"])
428 remote_status = tracker.getRemoteStatus("1")
429 self.assertEqual("open", remote_status)
430 lp_status = tracker.convertRemoteStatus(remote_status)
431@@ -306,6 +310,7 @@
432 lp_status = tracker.convertRemoteStatus(remote_status)
433 self.assertEqual(BugTaskStatus.NEW, lp_status)
434
435+ @responses.activate
436 def test_status_closed(self):
437 self.sample_bugs = [
438 {"id": 1, "state": "closed", "labels": []},
439@@ -313,9 +318,10 @@
440 {"id": 2, "state": "closed",
441 "labels": [{"name": "feature"}, {"name": "open"}]},
442 ]
443+ _add_rate_limit_response("api.github.com")
444+ self._addIssuesResponse()
445 tracker = GitHub("https://github.com/user/repository/issues")
446- with HTTMock(self._rate_limit_handler, self._issues_handler):
447- tracker.initializeRemoteBugDB(["1", "2"])
448+ tracker.initializeRemoteBugDB(["1", "2"])
449 remote_status = tracker.getRemoteStatus("1")
450 self.assertEqual("closed", remote_status)
451 lp_status = tracker.convertRemoteStatus(remote_status)
452@@ -330,22 +336,13 @@
453
454 layer = ZopelessDatabaseLayer
455
456- @urlmatch(path=r"^/rate_limit$")
457- def _rate_limit_handler(self, url, request):
458- self.rate_limit_request = request
459- rate_limit = {"limit": 5000, "remaining": 4000, "reset": 1000000000}
460- return {
461- "status_code": 200,
462- "content": {"resources": {"core": rate_limit}},
463- }
464-
465+ @responses.activate
466 def test_process_one(self):
467 remote_bug = {"id": 1234, "state": "open", "labels": []}
468-
469- @urlmatch(path=r".*/issues/1234$")
470- def handler(url, request):
471- return {"status_code": 200, "content": remote_bug}
472-
473+ _add_rate_limit_response("api.github.com")
474+ responses.add(
475+ "GET", "https://api.github.com/repos/user/repository/issues/1234",
476+ json=remote_bug)
477 bug = self.factory.makeBug()
478 bug_tracker = self.factory.makeBugTracker(
479 base_url="https://github.com/user/repository/issues",
480@@ -360,8 +357,7 @@
481 logger = BufferLogger()
482 bug_watch_updater = CheckwatchesMaster(transaction, logger=logger)
483 github = get_external_bugtracker(bug_tracker)
484- with HTTMock(self._rate_limit_handler, handler):
485- bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
486+ bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
487 self.assertEqual(
488 "INFO Updating 1 watches for 1 bugs on "
489 "https://api.github.com/repos/user/repository\n",
490@@ -371,17 +367,17 @@
491 [(watch.remotebug, github.convertRemoteStatus(watch.remotestatus))
492 for watch in bug_tracker.watches])
493
494+ @responses.activate
495 def test_process_many(self):
496 remote_bugs = [
497 {"id": bug_id,
498 "state": "open" if (bug_id % 2) == 0 else "closed",
499 "labels": []}
500 for bug_id in range(1000, 1010)]
501-
502- @urlmatch(path=r".*/issues$")
503- def handler(url, request):
504- return {"status_code": 200, "content": json.dumps(remote_bugs)}
505-
506+ _add_rate_limit_response("api.github.com")
507+ responses.add(
508+ "GET", "https://api.github.com/repos/user/repository/issues",
509+ json=remote_bugs)
510 bug = self.factory.makeBug()
511 bug_tracker = self.factory.makeBugTracker(
512 base_url="https://github.com/user/repository/issues",
513@@ -394,8 +390,7 @@
514 logger = BufferLogger()
515 bug_watch_updater = CheckwatchesMaster(transaction, logger=logger)
516 github = get_external_bugtracker(bug_tracker)
517- with HTTMock(self._rate_limit_handler, handler):
518- bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
519+ bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
520 self.assertEqual(
521 "INFO Updating 10 watches for 10 bugs on "
522 "https://api.github.com/repos/user/repository\n",
523
524=== modified file 'lib/lp/code/model/tests/test_githosting.py'
525--- lib/lp/code/model/tests/test_githosting.py 2018-03-31 13:30:36 +0000
526+++ lib/lp/code/model/tests/test_githosting.py 2018-05-31 10:48:11 +0000
527@@ -17,12 +17,10 @@
528
529 from contextlib import contextmanager
530 import json
531+import re
532
533-from httmock import (
534- all_requests,
535- HTTMock,
536- )
537 from lazr.restful.utils import get_current_browser_request
538+import responses
539 from testtools.matchers import MatchesStructure
540 from zope.component import getUtility
541 from zope.interface import implementer
542@@ -62,22 +60,12 @@
543 super(TestGitHostingClient, self).setUp()
544 self.client = getUtility(IGitHostingClient)
545 self.endpoint = removeSecurityProxy(self.client).endpoint
546- self.request = None
547+ self.requests = []
548
549 @contextmanager
550- def mockRequests(self, status_code=200, content=b"", reason=None,
551- set_default_timeout=True):
552- @all_requests
553- def handler(url, request):
554- self.assertIsNone(self.request)
555- self.request = request
556- return {
557- "status_code": status_code,
558- "content": content,
559- "reason": reason,
560- }
561-
562- with HTTMock(handler):
563+ def mockRequests(self, method, set_default_timeout=True, **kwargs):
564+ with responses.RequestsMock() as requests_mock:
565+ requests_mock.add(method, re.compile(r".*"), **kwargs)
566 original_timeout_function = get_default_timeout_function()
567 if set_default_timeout:
568 set_default_timeout_function(lambda: 60.0)
569@@ -85,12 +73,14 @@
570 yield
571 finally:
572 set_default_timeout_function(original_timeout_function)
573+ self.requests = [call.request for call in requests_mock.calls]
574
575 def assertRequest(self, url_suffix, json_data=None, method=None, **kwargs):
576- self.assertThat(self.request, MatchesStructure.byEquality(
577+ [request] = self.requests
578+ self.assertThat(request, MatchesStructure.byEquality(
579 url=urlappend(self.endpoint, url_suffix), method=method, **kwargs))
580 if json_data is not None:
581- self.assertEqual(json_data, json.loads(self.request.body))
582+ self.assertEqual(json_data, json.loads(request.body))
583 timeline = get_request_timeline(get_current_browser_request())
584 action = timeline.actions[-1]
585 self.assertEqual("git-hosting-%s" % method.lower(), action.category)
586@@ -98,94 +88,94 @@
587 "/" + url_suffix.split("?", 1)[0], action.detail.split(" ", 1)[0])
588
589 def test_create(self):
590- with self.mockRequests():
591+ with self.mockRequests("POST"):
592 self.client.create("123")
593 self.assertRequest(
594 "repo", method="POST", json_data={"repo_path": "123"})
595
596 def test_create_clone_from(self):
597- with self.mockRequests():
598+ with self.mockRequests("POST"):
599 self.client.create("123", clone_from="122")
600 self.assertRequest(
601 "repo", method="POST",
602 json_data={"repo_path": "123", "clone_from": "122"})
603
604 def test_create_failure(self):
605- with self.mockRequests(status_code=400, reason=b"Bad request"):
606+ with self.mockRequests("POST", status=400):
607 self.assertRaisesWithContent(
608 GitRepositoryCreationFault,
609 "Failed to create Git repository: "
610- "400 Client Error: Bad request",
611+ "400 Client Error: Bad Request",
612 self.client.create, "123")
613
614 def test_getProperties(self):
615 with self.mockRequests(
616- content=b'{"default_branch": "refs/heads/master"}'):
617+ "GET", json={"default_branch": "refs/heads/master"}):
618 props = self.client.getProperties("123")
619 self.assertEqual({"default_branch": "refs/heads/master"}, props)
620 self.assertRequest("repo/123", method="GET")
621
622 def test_getProperties_failure(self):
623- with self.mockRequests(status_code=400, reason=b"Bad request"):
624+ with self.mockRequests("GET", status=400):
625 self.assertRaisesWithContent(
626 GitRepositoryScanFault,
627 "Failed to get properties of Git repository: "
628- "400 Client Error: Bad request",
629+ "400 Client Error: Bad Request",
630 self.client.getProperties, "123")
631
632 def test_setProperties(self):
633- with self.mockRequests():
634+ with self.mockRequests("PATCH"):
635 self.client.setProperties("123", default_branch="refs/heads/a")
636 self.assertRequest(
637 "repo/123", method="PATCH",
638 json_data={"default_branch": "refs/heads/a"})
639
640 def test_setProperties_failure(self):
641- with self.mockRequests(status_code=400, reason=b"Bad request"):
642+ with self.mockRequests("PATCH", status=400):
643 self.assertRaisesWithContent(
644 GitRepositoryScanFault,
645 "Failed to set properties of Git repository: "
646- "400 Client Error: Bad request",
647+ "400 Client Error: Bad Request",
648 self.client.setProperties, "123",
649 default_branch="refs/heads/a")
650
651 def test_getRefs(self):
652- with self.mockRequests(content=b'{"refs/heads/master": {}}'):
653+ with self.mockRequests("GET", json={"refs/heads/master": {}}):
654 refs = self.client.getRefs("123")
655 self.assertEqual({"refs/heads/master": {}}, refs)
656 self.assertRequest("repo/123/refs", method="GET")
657
658 def test_getRefs_failure(self):
659- with self.mockRequests(status_code=400, reason=b"Bad request"):
660+ with self.mockRequests("GET", status=400):
661 self.assertRaisesWithContent(
662 GitRepositoryScanFault,
663 "Failed to get refs from Git repository: "
664- "400 Client Error: Bad request",
665+ "400 Client Error: Bad Request",
666 self.client.getRefs, "123")
667
668 def test_getCommits(self):
669- with self.mockRequests(content=b'[{"sha1": "0"}]'):
670+ with self.mockRequests("POST", json=[{"sha1": "0"}]):
671 commits = self.client.getCommits("123", ["0"])
672 self.assertEqual([{"sha1": "0"}], commits)
673 self.assertRequest(
674 "repo/123/commits", method="POST", json_data={"commits": ["0"]})
675
676 def test_getCommits_failure(self):
677- with self.mockRequests(status_code=400, reason=b"Bad request"):
678+ with self.mockRequests("POST", status=400):
679 self.assertRaisesWithContent(
680 GitRepositoryScanFault,
681 "Failed to get commit details from Git repository: "
682- "400 Client Error: Bad request",
683+ "400 Client Error: Bad Request",
684 self.client.getCommits, "123", ["0"])
685
686 def test_getLog(self):
687- with self.mockRequests(content=b'[{"sha1": "0"}]'):
688+ with self.mockRequests("GET", json=[{"sha1": "0"}]):
689 log = self.client.getLog("123", "refs/heads/master")
690 self.assertEqual([{"sha1": "0"}], log)
691 self.assertRequest("repo/123/log/refs/heads/master", method="GET")
692
693 def test_getLog_limit_stop(self):
694- with self.mockRequests(content=b'[{"sha1": "0"}]'):
695+ with self.mockRequests("GET", json=[{"sha1": "0"}]):
696 log = self.client.getLog(
697 "123", "refs/heads/master", limit=10, stop="refs/heads/old")
698 self.assertEqual([{"sha1": "0"}], log)
699@@ -194,48 +184,48 @@
700 method="GET")
701
702 def test_getLog_failure(self):
703- with self.mockRequests(status_code=400, reason=b"Bad request"):
704+ with self.mockRequests("GET", status=400):
705 self.assertRaisesWithContent(
706 GitRepositoryScanFault,
707 "Failed to get commit log from Git repository: "
708- "400 Client Error: Bad request",
709+ "400 Client Error: Bad Request",
710 self.client.getLog, "123", "refs/heads/master")
711
712 def test_getDiff(self):
713- with self.mockRequests(content=b'{"patch": ""}'):
714+ with self.mockRequests("GET", json={"patch": ""}):
715 diff = self.client.getDiff("123", "a", "b")
716 self.assertEqual({"patch": ""}, diff)
717 self.assertRequest("repo/123/compare/a..b", method="GET")
718
719 def test_getDiff_common_ancestor(self):
720- with self.mockRequests(content=b'{"patch": ""}'):
721+ with self.mockRequests("GET", json={"patch": ""}):
722 diff = self.client.getDiff("123", "a", "b", common_ancestor=True)
723 self.assertEqual({"patch": ""}, diff)
724 self.assertRequest("repo/123/compare/a...b", method="GET")
725
726 def test_getDiff_context_lines(self):
727- with self.mockRequests(content=b'{"patch": ""}'):
728+ with self.mockRequests("GET", json={"patch": ""}):
729 diff = self.client.getDiff("123", "a", "b", context_lines=4)
730 self.assertEqual({"patch": ""}, diff)
731 self.assertRequest(
732 "repo/123/compare/a..b?context_lines=4", method="GET")
733
734 def test_getDiff_failure(self):
735- with self.mockRequests(status_code=400, reason=b"Bad request"):
736+ with self.mockRequests("GET", status=400):
737 self.assertRaisesWithContent(
738 GitRepositoryScanFault,
739 "Failed to get diff from Git repository: "
740- "400 Client Error: Bad request",
741+ "400 Client Error: Bad Request",
742 self.client.getDiff, "123", "a", "b")
743
744 def test_getMergeDiff(self):
745- with self.mockRequests(content=b'{"patch": ""}'):
746+ with self.mockRequests("GET", json={"patch": ""}):
747 diff = self.client.getMergeDiff("123", "a", "b")
748 self.assertEqual({"patch": ""}, diff)
749 self.assertRequest("repo/123/compare-merge/a:b", method="GET")
750
751 def test_getMergeDiff_prerequisite(self):
752- with self.mockRequests(content=b'{"patch": ""}'):
753+ with self.mockRequests("GET", json={"patch": ""}):
754 diff = self.client.getMergeDiff("123", "a", "b", prerequisite="c")
755 self.assertEqual({"patch": ""}, diff)
756 self.assertRequest(
757@@ -245,23 +235,23 @@
758 # pygit2 tries to decode the diff as UTF-8 with errors="replace".
759 # In some cases this can result in unpaired surrogates, which older
760 # versions of json/simplejson don't like.
761- content = json.dumps(
762+ body = json.dumps(
763 {"patch": "卷。".encode("GBK").decode("UTF-8", errors="replace")})
764- with self.mockRequests(content=content):
765+ with self.mockRequests("GET", body=body):
766 diff = self.client.getMergeDiff("123", "a", "b")
767 self.assertEqual({"patch": "\uFFFD\uD863"}, diff)
768 self.assertRequest("repo/123/compare-merge/a:b", method="GET")
769
770 def test_getMergeDiff_failure(self):
771- with self.mockRequests(status_code=400, reason=b"Bad request"):
772+ with self.mockRequests("GET", status=400):
773 self.assertRaisesWithContent(
774 GitRepositoryScanFault,
775 "Failed to get merge diff from Git repository: "
776- "400 Client Error: Bad request",
777+ "400 Client Error: Bad Request",
778 self.client.getMergeDiff, "123", "a", "b")
779
780 def test_detectMerges(self):
781- with self.mockRequests(content=b'{"b": "0"}'):
782+ with self.mockRequests("POST", json={"b": "0"}):
783 merges = self.client.detectMerges("123", "a", ["b", "c"])
784 self.assertEqual({"b": "0"}, merges)
785 self.assertRequest(
786@@ -269,30 +259,30 @@
787 json_data={"sources": ["b", "c"]})
788
789 def test_detectMerges_failure(self):
790- with self.mockRequests(status_code=400, reason=b"Bad request"):
791+ with self.mockRequests("POST", status=400):
792 self.assertRaisesWithContent(
793 GitRepositoryScanFault,
794 "Failed to detect merges in Git repository: "
795- "400 Client Error: Bad request",
796+ "400 Client Error: Bad Request",
797 self.client.detectMerges, "123", "a", ["b", "c"])
798
799 def test_delete(self):
800- with self.mockRequests():
801+ with self.mockRequests("DELETE"):
802 self.client.delete("123")
803 self.assertRequest("repo/123", method="DELETE")
804
805 def test_delete_failed(self):
806- with self.mockRequests(status_code=400, reason=b"Bad request"):
807+ with self.mockRequests("DELETE", status=400):
808 self.assertRaisesWithContent(
809 GitRepositoryDeletionFault,
810 "Failed to delete Git repository: "
811- "400 Client Error: Bad request",
812+ "400 Client Error: Bad Request",
813 self.client.delete, "123")
814
815 def test_getBlob(self):
816 blob = b''.join(chr(i) for i in range(256))
817- content = {"data": blob.encode("base64"), "size": len(blob)}
818- with self.mockRequests(content=json.dumps(content)):
819+ payload = {"data": blob.encode("base64"), "size": len(blob)}
820+ with self.mockRequests("GET", json=payload):
821 response = self.client.getBlob("123", "dir/path/file/name")
822 self.assertEqual(blob, response)
823 self.assertRequest(
824@@ -300,22 +290,22 @@
825
826 def test_getBlob_revision(self):
827 blob = b''.join(chr(i) for i in range(256))
828- content = {"data": blob.encode("base64"), "size": len(blob)}
829- with self.mockRequests(content=json.dumps(content)):
830+ payload = {"data": blob.encode("base64"), "size": len(blob)}
831+ with self.mockRequests("GET", json=payload):
832 response = self.client.getBlob("123", "dir/path/file/name", "dev")
833 self.assertEqual(blob, response)
834 self.assertRequest(
835 "repo/123/blob/dir/path/file/name?rev=dev", method="GET")
836
837 def test_getBlob_not_found(self):
838- with self.mockRequests(status_code=404, reason=b"Not found"):
839+ with self.mockRequests("GET", status=404):
840 self.assertRaisesWithContent(
841 GitRepositoryBlobNotFound,
842 "Repository 123 has no file dir/path/file/name",
843 self.client.getBlob, "123", "dir/path/file/name")
844
845 def test_getBlob_revision_not_found(self):
846- with self.mockRequests(status_code=404, reason=b"Not found"):
847+ with self.mockRequests("GET", status=404):
848 self.assertRaisesWithContent(
849 GitRepositoryBlobNotFound,
850 "Repository 123 has no file dir/path/file/name "
851@@ -323,39 +313,38 @@
852 self.client.getBlob, "123", "dir/path/file/name", "dev")
853
854 def test_getBlob_failure(self):
855- with self.mockRequests(status_code=400, reason=b"Bad request"):
856+ with self.mockRequests("GET", status=400):
857 self.assertRaisesWithContent(
858 GitRepositoryScanFault,
859 "Failed to get file from Git repository: "
860- "400 Client Error: Bad request",
861+ "400 Client Error: Bad Request",
862 self.client.getBlob, "123", "dir/path/file/name")
863
864 def test_getBlob_url_quoting(self):
865 blob = b''.join(chr(i) for i in range(256))
866- content = {"data": blob.encode("base64"), "size": len(blob)}
867- with self.mockRequests(content=json.dumps(content)):
868+ payload = {"data": blob.encode("base64"), "size": len(blob)}
869+ with self.mockRequests("GET", json=payload):
870 self.client.getBlob("123", "dir/+file name?.txt", "+rev/ no?")
871 self.assertRequest(
872 "repo/123/blob/dir/%2Bfile%20name%3F.txt?rev=%2Brev%2F+no%3F",
873 method="GET")
874
875 def test_getBlob_no_data(self):
876- with self.mockRequests(content=json.dumps({"size": 1})):
877+ with self.mockRequests("GET", json={"size": 1}):
878 self.assertRaisesWithContent(
879 GitRepositoryScanFault,
880 "Failed to get file from Git repository: 'data'",
881 self.client.getBlob, "123", "dir/path/file/name")
882
883 def test_getBlob_no_size(self):
884- with self.mockRequests(content=json.dumps({"data": "data"})):
885+ with self.mockRequests("GET", json={"data": "data"}):
886 self.assertRaisesWithContent(
887 GitRepositoryScanFault,
888 "Failed to get file from Git repository: 'size'",
889 self.client.getBlob, "123", "dir/path/file/name")
890
891 def test_getBlob_bad_encoding(self):
892- content = {"data": "x", "size": 1}
893- with self.mockRequests(content=json.dumps(content)):
894+ with self.mockRequests("GET", json={"data": "x", "size": 1}):
895 self.assertRaisesWithContent(
896 GitRepositoryScanFault,
897 "Failed to get file from Git repository: Incorrect padding",
898@@ -363,8 +352,8 @@
899
900 def test_getBlob_wrong_size(self):
901 blob = b''.join(chr(i) for i in range(256))
902- content = {"data": blob.encode("base64"), "size": 0}
903- with self.mockRequests(content=json.dumps(content)):
904+ payload = {"data": blob.encode("base64"), "size": 0}
905+ with self.mockRequests("GET", json=payload):
906 self.assertRaisesWithContent(
907 GitRepositoryScanFault,
908 "Failed to get file from Git repository: Unexpected size"
909@@ -382,7 +371,7 @@
910
911 def run(self):
912 with self.testcase.mockRequests(
913- content=b'{"refs/heads/master": {}}',
914+ "GET", json={"refs/heads/master": {}},
915 set_default_timeout=False):
916 self.refs = self.testcase.client.getRefs("123")
917 # We must make this assertion inside the job, since the job
918
919=== modified file 'lib/lp/services/webhooks/tests/test_job.py'
920--- lib/lp/services/webhooks/tests/test_job.py 2017-03-28 23:56:03 +0000
921+++ lib/lp/services/webhooks/tests/test_job.py 2018-05-31 10:48:11 +0000
922@@ -1,4 +1,4 @@
923-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
924+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
925 # GNU Affero General Public License version 3 (see the file LICENSE).
926
927 """Tests for `WebhookJob`s."""
928@@ -9,14 +9,12 @@
929 datetime,
930 timedelta,
931 )
932+import re
933
934-from httmock import (
935- HTTMock,
936- urlmatch,
937- )
938 from pytz import utc
939 import requests
940 import requests.exceptions
941+import responses
942 from storm.store import Store
943 from testtools import TestCase
944 from testtools.matchers import (
945@@ -139,25 +137,18 @@
946 class TestWebhookClient(TestCase):
947 """Tests for `WebhookClient`."""
948
949- def sendToWebhook(self, response_status=200, raises=None, headers=None):
950- reqs = []
951-
952- @urlmatch(netloc='example.com')
953- def endpoint_mock(url, request):
954- if raises:
955- raise raises
956- reqs.append(request)
957- return {
958- 'status_code': response_status, 'content': 'Content',
959- 'headers': headers}
960-
961- with HTTMock(endpoint_mock):
962+ def sendToWebhook(self, body='Content', **kwargs):
963+ with responses.RequestsMock() as requests_mock:
964+ requests_mock.add(
965+ 'POST', re.compile('^http://example\.com/'), body=body,
966+ **kwargs)
967 result = WebhookClient().deliver(
968 'http://example.com/ep', 'http://squid.example.com:3128',
969 'TestWebhookClient', 30, 'sekrit', '1234', 'test',
970 {'foo': 'bar'})
971+ calls = list(requests_mock.calls)
972
973- return reqs, result
974+ return calls, result
975
976 @property
977 def request_matcher(self):
978@@ -177,27 +168,27 @@
979 })
980
981 def test_sends_request(self):
982- [request], result = self.sendToWebhook()
983+ [call], result = self.sendToWebhook()
984 self.assertThat(
985 result,
986 MatchesDict({
987 'request': self.request_matcher,
988 'response': MatchesDict({
989 'status_code': Equals(200),
990- 'headers': Equals({}),
991+ 'headers': Equals({'content-type': 'text/plain'}),
992 'body': Equals('Content'),
993 }),
994 }))
995
996 def test_accepts_404(self):
997- [request], result = self.sendToWebhook(response_status=404)
998+ [call], result = self.sendToWebhook(status=404)
999 self.assertThat(
1000 result,
1001 MatchesDict({
1002 'request': self.request_matcher,
1003 'response': MatchesDict({
1004 'status_code': Equals(404),
1005- 'headers': Equals({}),
1006+ 'headers': Equals({'content-type': 'text/plain'}),
1007 'body': Equals('Content'),
1008 }),
1009 }))
1010@@ -205,35 +196,34 @@
1011 def test_connection_error(self):
1012 # Attempts that fail to connect have a connection_error rather
1013 # than a response.
1014- reqs, result = self.sendToWebhook(
1015- raises=requests.ConnectionError('Connection refused'))
1016+ [call], result = self.sendToWebhook(
1017+ body=requests.ConnectionError('Connection refused'))
1018 self.assertThat(
1019 result,
1020 MatchesDict({
1021 'request': self.request_matcher,
1022 'connection_error': Equals('Connection refused'),
1023 }))
1024- self.assertEqual([], reqs)
1025+ self.assertIsInstance(call.response, requests.ConnectionError)
1026
1027 def test_timeout_error(self):
1028 # Attempts that don't return within the timeout have a
1029 # connection_error rather than a response.
1030- reqs, result = self.sendToWebhook(
1031- raises=requests.exceptions.ReadTimeout())
1032+ [call], result = self.sendToWebhook(
1033+ body=requests.exceptions.ReadTimeout())
1034 self.assertThat(
1035 result,
1036 MatchesDict({
1037 'request': self.request_matcher,
1038 'connection_error': Equals('Request timeout'),
1039 }))
1040- self.assertEqual([], reqs)
1041+ self.assertIsInstance(call.response, requests.exceptions.ReadTimeout)
1042
1043 def test_proxy_error_known(self):
1044 # Squid error headers are interpreted to populate
1045 # connection_error.
1046- [request], result = self.sendToWebhook(
1047- response_status=403,
1048- headers={"X-Squid-Error": "ERR_ACCESS_DENIED 0"})
1049+ [call], result = self.sendToWebhook(
1050+ status=403, headers={"X-Squid-Error": "ERR_ACCESS_DENIED 0"})
1051 self.assertThat(
1052 result,
1053 MatchesDict({
1054@@ -244,9 +234,8 @@
1055 def test_proxy_error_unknown(self):
1056 # Squid errors that don't have a human-readable mapping are
1057 # included verbatim.
1058- [request], result = self.sendToWebhook(
1059- response_status=403,
1060- headers={"X-Squid-Error": "ERR_BORKED 1234"})
1061+ [call], result = self.sendToWebhook(
1062+ status=403, headers={"X-Squid-Error": "ERR_BORKED 1234"})
1063 self.assertThat(
1064 result,
1065 MatchesDict({
1066
1067=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
1068--- lib/lp/snappy/browser/tests/test_snap.py 2018-05-07 05:25:27 +0000
1069+++ lib/lp/snappy/browser/tests/test_snap.py 2018-05-31 10:48:11 +0000
1070@@ -20,14 +20,11 @@
1071 )
1072
1073 from fixtures import FakeLogger
1074-from httmock import (
1075- all_requests,
1076- HTTMock,
1077- )
1078 from mechanize import LinkNotFoundError
1079 import mock
1080 from pymacaroons import Macaroon
1081 import pytz
1082+import responses
1083 import soupmatchers
1084 from testtools.matchers import (
1085 MatchesSetwise,
1086@@ -403,6 +400,7 @@
1087 "Pocket for automatic builds:\nSecurity\nEdit snap package",
1088 MatchesTagText(content, "auto_build_pocket"))
1089
1090+ @responses.activate
1091 def test_create_new_snap_store_upload(self):
1092 # Creating a new snap and asking for it to be automatically uploaded
1093 # to the store sets all the appropriate fields and redirects to SSO
1094@@ -422,19 +420,12 @@
1095 urlsplit(config.launchpad.openid_provider_root).netloc, "",
1096 "dummy")
1097 root_macaroon_raw = root_macaroon.serialize()
1098-
1099- @all_requests
1100- def handler(url, request):
1101- self.request = request
1102- return {
1103- "status_code": 200,
1104- "content": {"macaroon": root_macaroon_raw},
1105- }
1106-
1107 self.pushConfig("snappy", store_url="http://sca.example/")
1108- with HTTMock(handler):
1109- redirection = self.assertRaises(
1110- HTTPError, browser.getControl("Create snap package").click)
1111+ responses.add(
1112+ "POST", "http://sca.example/dev/api/acl/",
1113+ json={"macaroon": root_macaroon_raw})
1114+ redirection = self.assertRaises(
1115+ HTTPError, browser.getControl("Create snap package").click)
1116 login_person(self.person)
1117 snap = getUtility(ISnapSet).getByName(self.person, "snap-name")
1118 self.assertThat(snap, MatchesStructure.byEquality(
1119@@ -443,7 +434,8 @@
1120 store_series=self.snappyseries, store_name="store-name",
1121 store_secrets={"root": root_macaroon_raw},
1122 store_channels=["track/edge"]))
1123- self.assertThat(self.request, MatchesStructure.byEquality(
1124+ [call] = responses.calls
1125+ self.assertThat(call.request, MatchesStructure.byEquality(
1126 url="http://sca.example/dev/api/acl/", method="POST"))
1127 expected_body = {
1128 "packages": [{
1129@@ -452,7 +444,7 @@
1130 }],
1131 "permissions": ["package_upload"],
1132 }
1133- self.assertEqual(expected_body, json.loads(self.request.body))
1134+ self.assertEqual(expected_body, json.loads(call.request.body))
1135 self.assertEqual(303, redirection.code)
1136 parsed_location = urlsplit(redirection.hdrs["Location"])
1137 self.assertEqual(
1138@@ -969,6 +961,7 @@
1139 self.assertNeedStoreReauth(
1140 True, {"store_upload": False}, {"store_upload": True})
1141
1142+ @responses.activate
1143 def test_edit_store_upload(self):
1144 # Changing store upload settings on a snap sets all the appropriate
1145 # fields and redirects to SSO for reauthorization.
1146@@ -989,30 +982,24 @@
1147 urlsplit(config.launchpad.openid_provider_root).netloc, "",
1148 "dummy")
1149 root_macaroon_raw = root_macaroon.serialize()
1150-
1151- @all_requests
1152- def handler(url, request):
1153- self.request = request
1154- return {
1155- "status_code": 200,
1156- "content": {"macaroon": root_macaroon_raw},
1157- }
1158-
1159 self.pushConfig("snappy", store_url="http://sca.example/")
1160- with HTTMock(handler):
1161- redirection = self.assertRaises(
1162- HTTPError, browser.getControl("Update snap package").click)
1163+ responses.add(
1164+ "POST", "http://sca.example/dev/api/acl/",
1165+ json={"macaroon": root_macaroon_raw})
1166+ redirection = self.assertRaises(
1167+ HTTPError, browser.getControl("Update snap package").click)
1168 login_person(self.person)
1169 self.assertThat(snap, MatchesStructure.byEquality(
1170 store_name="two", store_secrets={"root": root_macaroon_raw},
1171 store_channels=["stable", "edge"]))
1172- self.assertThat(self.request, MatchesStructure.byEquality(
1173+ [call] = responses.calls
1174+ self.assertThat(call.request, MatchesStructure.byEquality(
1175 url="http://sca.example/dev/api/acl/", method="POST"))
1176 expected_body = {
1177 "packages": [{"name": "two", "series": self.snappyseries.name}],
1178 "permissions": ["package_upload"],
1179 }
1180- self.assertEqual(expected_body, json.loads(self.request.body))
1181+ self.assertEqual(expected_body, json.loads(call.request.body))
1182 self.assertEqual(303, redirection.code)
1183 parsed_location = urlsplit(redirection.hdrs["Location"])
1184 self.assertEqual(
1185@@ -1047,6 +1034,7 @@
1186 Unauthorized, self.getUserBrowser,
1187 canonical_url(self.snap) + "/+authorize", user=other_person)
1188
1189+ @responses.activate
1190 def test_begin_authorization(self):
1191 # With no special form actions, we return a form inviting the user
1192 # to begin authorization. This allows (re-)authorizing uploads of
1193@@ -1058,22 +1046,16 @@
1194 urlsplit(config.launchpad.openid_provider_root).netloc, '',
1195 'dummy')
1196 root_macaroon_raw = root_macaroon.serialize()
1197-
1198- @all_requests
1199- def handler(url, request):
1200- self.request = request
1201- return {
1202- "status_code": 200,
1203- "content": {"macaroon": root_macaroon_raw},
1204- }
1205-
1206 self.pushConfig("snappy", store_url="http://sca.example/")
1207- with HTTMock(handler):
1208- browser = self.getNonRedirectingBrowser(
1209- url=snap_url + "/+authorize", user=self.snap.owner)
1210- redirection = self.assertRaises(
1211- HTTPError, browser.getControl("Begin authorization").click)
1212- self.assertThat(self.request, MatchesStructure.byEquality(
1213+ responses.add(
1214+ "POST", "http://sca.example/dev/api/acl/",
1215+ json={"macaroon": root_macaroon_raw})
1216+ browser = self.getNonRedirectingBrowser(
1217+ url=snap_url + "/+authorize", user=self.snap.owner)
1218+ redirection = self.assertRaises(
1219+ HTTPError, browser.getControl("Begin authorization").click)
1220+ [call] = responses.calls
1221+ self.assertThat(call.request, MatchesStructure.byEquality(
1222 url="http://sca.example/dev/api/acl/", method="POST"))
1223 with person_logged_in(owner):
1224 expected_body = {
1225@@ -1083,7 +1065,7 @@
1226 }],
1227 "permissions": ["package_upload"],
1228 }
1229- self.assertEqual(expected_body, json.loads(self.request.body))
1230+ self.assertEqual(expected_body, json.loads(call.request.body))
1231 self.assertEqual(
1232 {"root": root_macaroon_raw}, self.snap.store_secrets)
1233 self.assertEqual(303, redirection.code)
1234
1235=== modified file 'lib/lp/snappy/tests/test_snap.py'
1236--- lib/lp/snappy/tests/test_snap.py 2018-04-21 10:01:22 +0000
1237+++ lib/lp/snappy/tests/test_snap.py 2018-05-31 10:48:11 +0000
1238@@ -14,13 +14,10 @@
1239 import json
1240 from urlparse import urlsplit
1241
1242-from httmock import (
1243- all_requests,
1244- HTTMock,
1245- )
1246 from lazr.lifecycle.event import ObjectModifiedEvent
1247 from pymacaroons import Macaroon
1248 import pytz
1249+import responses
1250 from storm.exceptions import LostObjectError
1251 from storm.locals import Store
1252 from testtools.matchers import (
1253@@ -1919,21 +1916,16 @@
1254 urlsplit(config.launchpad.openid_provider_root).netloc, '',
1255 'dummy')
1256 root_macaroon_raw = root_macaroon.serialize()
1257-
1258- @all_requests
1259- def handler(url, request):
1260- self.request = request
1261- return {
1262- "status_code": 200,
1263- "content": {"macaroon": root_macaroon_raw},
1264- }
1265-
1266 self.pushConfig("snappy", store_url="http://sca.example/")
1267 logout()
1268- with HTTMock(handler):
1269+ with responses.RequestsMock() as requests_mock:
1270+ requests_mock.add(
1271+ "POST", "http://sca.example/dev/api/acl/",
1272+ json={"macaroon": root_macaroon_raw})
1273 response = self.webservice.named_post(
1274 snap_url, "beginAuthorization", **kwargs)
1275- self.assertThat(self.request, MatchesStructure.byEquality(
1276+ [call] = requests_mock.calls
1277+ self.assertThat(call.request, MatchesStructure.byEquality(
1278 url="http://sca.example/dev/api/acl/", method="POST"))
1279 with person_logged_in(self.person):
1280 expected_body = {
1281@@ -1943,7 +1935,7 @@
1282 }],
1283 "permissions": ["package_upload"],
1284 }
1285- self.assertEqual(expected_body, json.loads(self.request.body))
1286+ self.assertEqual(expected_body, json.loads(call.request.body))
1287 self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
1288 return response, root_macaroon.third_party_caveats()[0]
1289
1290
1291=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
1292--- lib/lp/snappy/tests/test_snapstoreclient.py 2018-05-02 23:55:51 +0000
1293+++ lib/lp/snappy/tests/test_snapstoreclient.py 2018-05-31 10:48:11 +0000
1294@@ -13,11 +13,6 @@
1295 import io
1296 import json
1297
1298-from httmock import (
1299- all_requests,
1300- HTTMock,
1301- urlmatch,
1302- )
1303 from lazr.restful.utils import get_current_browser_request
1304 from pymacaroons import (
1305 Macaroon,
1306@@ -25,6 +20,7 @@
1307 )
1308 from requests import Request
1309 from requests.utils import parse_dict_header
1310+import responses
1311 from testtools.matchers import (
1312 Contains,
1313 ContainsDict,
1314@@ -231,6 +227,7 @@
1315 {"name": "stable", "display_name": "Stable"},
1316 {"name": "edge", "display_name": "Edge"},
1317 ]
1318+ self.channels_memcache_key = "search.example:channels".encode("UTF-8")
1319
1320 def _make_store_secrets(self):
1321 self.root_key = hashlib.sha256(
1322@@ -249,51 +246,45 @@
1323 "discharge": unbound_discharge_macaroon.serialize(),
1324 }
1325
1326- @urlmatch(path=r".*/unscanned-upload/$")
1327- def _unscanned_upload_handler(self, url, request):
1328- self.unscanned_upload_requests.append(request)
1329- return {
1330- "status_code": 200,
1331- "content": {"successful": True, "upload_id": 1},
1332- }
1333+ def _addUnscannedUploadResponse(self):
1334+ responses.add(
1335+ "POST", "http://updown.example/unscanned-upload/",
1336+ json={"successful": True, "upload_id": 1})
1337
1338- @urlmatch(path=r".*/snap-push/$")
1339- def _snap_push_handler(self, url, request):
1340- self.snap_push_request = request
1341- return {
1342- "status_code": 202,
1343- "content": {
1344+ def _addSnapPushResponse(self):
1345+ responses.add(
1346+ "POST", "http://sca.example/dev/api/snap-push/", status=202,
1347+ json={
1348 "success": True,
1349 "status_details_url": (
1350 "http://sca.example/dev/api/snaps/1/builds/1/status"),
1351- }}
1352-
1353- @urlmatch(path=r".*/api/v2/tokens/refresh$")
1354- def _macaroon_refresh_handler(self, url, request):
1355- self.refresh_request = request
1356- new_macaroon = Macaroon(
1357- location="sso.example", key=self.discharge_key,
1358- identifier=self.discharge_caveat_id)
1359- new_macaroon.add_first_party_caveat("sso|expires|tomorrow")
1360- return {
1361- "status_code": 200,
1362- "content": {"discharge_macaroon": new_macaroon.serialize()},
1363- }
1364-
1365- @urlmatch(path=r".*/api/v1/channels$")
1366- def _channels_handler(self, url, request):
1367- self.channels_request = request
1368- return {
1369- "status_code": 200,
1370- "content": {"_embedded": {"clickindex:channel": self.channels}},
1371- }
1372-
1373- @urlmatch(path=r".*/snap-release/$")
1374- def _snap_release_handler(self, url, request):
1375- self.snap_release_request = request
1376- return {
1377- "status_code": 200,
1378- "content": {
1379+ })
1380+
1381+ def _addMacaroonRefreshResponse(self):
1382+ def callback(request):
1383+ new_macaroon = Macaroon(
1384+ location="sso.example", key=self.discharge_key,
1385+ identifier=self.discharge_caveat_id)
1386+ new_macaroon.add_first_party_caveat("sso|expires|tomorrow")
1387+ return (
1388+ 200, {},
1389+ json.dumps({"discharge_macaroon": new_macaroon.serialize()}))
1390+
1391+ responses.add_callback(
1392+ "POST", "http://sso.example/api/v2/tokens/refresh",
1393+ callback=callback, content_type="application/json")
1394+
1395+ def _addChannelsResponse(self):
1396+ responses.add(
1397+ "GET", "http://search.example/api/v1/channels",
1398+ json={"_embedded": {"clickindex:channel": self.channels}})
1399+ self.addCleanup(
1400+ getUtility(IMemcacheClient).delete, self.channels_memcache_key)
1401+
1402+ def _addSnapReleaseResponse(self):
1403+ responses.add(
1404+ "POST", "http://sca.example/dev/api/snap-release/",
1405+ json={
1406 "success": True,
1407 "channel_map": [
1408 {"channel": "stable", "info": "specific",
1409@@ -302,19 +293,17 @@
1410 "version": "1.0", "revision": 1},
1411 ],
1412 "opened_channels": ["stable", "edge"],
1413- }}
1414+ })
1415
1416+ @responses.activate
1417 def test_requestPackageUploadPermission(self):
1418- @all_requests
1419- def handler(url, request):
1420- self.request = request
1421- return {"status_code": 200, "content": {"macaroon": "dummy"}}
1422-
1423 snappy_series = self.factory.makeSnappySeries(name="rolling")
1424- with HTTMock(handler):
1425- macaroon = self.client.requestPackageUploadPermission(
1426- snappy_series, "test-snap")
1427- self.assertThat(self.request, RequestMatches(
1428+ responses.add(
1429+ "POST", "http://sca.example/dev/api/acl/",
1430+ json={"macaroon": "dummy"})
1431+ macaroon = self.client.requestPackageUploadPermission(
1432+ snappy_series, "test-snap")
1433+ self.assertThat(responses.calls[-1].request, RequestMatches(
1434 url=Equals("http://sca.example/dev/api/acl/"),
1435 method=Equals("POST"),
1436 json_data={
1437@@ -329,45 +318,35 @@
1438 self.assertEqual("request-snap-upload-macaroon-stop", stop.category)
1439 self.assertEqual("rolling/test-snap", stop.detail)
1440
1441+ @responses.activate
1442 def test_requestPackageUploadPermission_missing_macaroon(self):
1443- @all_requests
1444- def handler(url, request):
1445- return {"status_code": 200, "content": {}}
1446-
1447 snappy_series = self.factory.makeSnappySeries()
1448- with HTTMock(handler):
1449- self.assertRaisesWithContent(
1450- BadRequestPackageUploadResponse, b"{}",
1451- self.client.requestPackageUploadPermission,
1452- snappy_series, "test-snap")
1453+ responses.add("POST", "http://sca.example/dev/api/acl/", json={})
1454+ self.assertRaisesWithContent(
1455+ BadRequestPackageUploadResponse, b"{}",
1456+ self.client.requestPackageUploadPermission,
1457+ snappy_series, "test-snap")
1458
1459+ @responses.activate
1460 def test_requestPackageUploadPermission_error(self):
1461- @all_requests
1462- def handler(url, request):
1463- return {
1464- "status_code": 503,
1465- "content": {"error_list": [{"message": "Failed"}]},
1466- }
1467-
1468 snappy_series = self.factory.makeSnappySeries()
1469- with HTTMock(handler):
1470- self.assertRaisesWithContent(
1471- BadRequestPackageUploadResponse, "Failed",
1472- self.client.requestPackageUploadPermission,
1473- snappy_series, "test-snap")
1474+ responses.add(
1475+ "POST", "http://sca.example/dev/api/acl/",
1476+ status=503, json={"error_list": [{"message": "Failed"}]})
1477+ self.assertRaisesWithContent(
1478+ BadRequestPackageUploadResponse, "Failed",
1479+ self.client.requestPackageUploadPermission,
1480+ snappy_series, "test-snap")
1481
1482+ @responses.activate
1483 def test_requestPackageUploadPermission_404(self):
1484- @all_requests
1485- def handler(url, request):
1486- return {"status_code": 404, "reason": b"Not found"}
1487-
1488 snappy_series = self.factory.makeSnappySeries()
1489- with HTTMock(handler):
1490- self.assertRaisesWithContent(
1491- BadRequestPackageUploadResponse,
1492- b"404 Client Error: Not found",
1493- self.client.requestPackageUploadPermission,
1494- snappy_series, "test-snap")
1495+ responses.add("POST", "http://sca.example/dev/api/acl/", status=404)
1496+ self.assertRaisesWithContent(
1497+ BadRequestPackageUploadResponse,
1498+ b"404 Client Error: Not Found",
1499+ self.client.requestPackageUploadPermission,
1500+ snappy_series, "test-snap")
1501
1502 def makeUploadableSnapBuild(self, store_secrets=None):
1503 if store_secrets is None:
1504@@ -386,16 +365,18 @@
1505 snapbuild=snapbuild, libraryfile=manifest_lfa)
1506 return snapbuild
1507
1508+ @responses.activate
1509 def test_upload(self):
1510 snapbuild = self.makeUploadableSnapBuild()
1511 transaction.commit()
1512+ self._addUnscannedUploadResponse()
1513+ self._addSnapPushResponse()
1514 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1515- with HTTMock(self._unscanned_upload_handler,
1516- self._snap_push_handler):
1517- self.assertEqual(
1518- "http://sca.example/dev/api/snaps/1/builds/1/status",
1519- self.client.upload(snapbuild))
1520- self.assertThat(self.unscanned_upload_requests, MatchesListwise([
1521+ self.assertEqual(
1522+ "http://sca.example/dev/api/snaps/1/builds/1/status",
1523+ self.client.upload(snapbuild))
1524+ requests = [call.request for call in responses.calls]
1525+ self.assertThat(requests, MatchesListwise([
1526 RequestMatches(
1527 url=Equals("http://updown.example/unscanned-upload/"),
1528 method=Equals("POST"),
1529@@ -404,29 +385,33 @@
1530 name="binary", filename="test-snap.snap",
1531 value="dummy snap content",
1532 type="application/octet-stream",
1533- )})]))
1534- self.assertThat(self.snap_push_request, RequestMatches(
1535- url=Equals("http://sca.example/dev/api/snap-push/"),
1536- method=Equals("POST"),
1537- headers=ContainsDict({"Content-Type": Equals("application/json")}),
1538- auth=("Macaroon", MacaroonsVerify(self.root_key)),
1539- json_data={
1540- "name": "test-snap", "updown_id": 1, "series": "rolling",
1541- }))
1542+ )}),
1543+ RequestMatches(
1544+ url=Equals("http://sca.example/dev/api/snap-push/"),
1545+ method=Equals("POST"),
1546+ headers=ContainsDict(
1547+ {"Content-Type": Equals("application/json")}),
1548+ auth=("Macaroon", MacaroonsVerify(self.root_key)),
1549+ json_data={
1550+ "name": "test-snap", "updown_id": 1, "series": "rolling",
1551+ }),
1552+ ]))
1553
1554+ @responses.activate
1555 def test_upload_no_discharge(self):
1556 root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
1557 root_macaroon = Macaroon(key=root_key)
1558 snapbuild = self.makeUploadableSnapBuild(
1559 store_secrets={"root": root_macaroon.serialize()})
1560 transaction.commit()
1561+ self._addUnscannedUploadResponse()
1562+ self._addSnapPushResponse()
1563 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1564- with HTTMock(self._unscanned_upload_handler,
1565- self._snap_push_handler):
1566- self.assertEqual(
1567- "http://sca.example/dev/api/snaps/1/builds/1/status",
1568- self.client.upload(snapbuild))
1569- self.assertThat(self.unscanned_upload_requests, MatchesListwise([
1570+ self.assertEqual(
1571+ "http://sca.example/dev/api/snaps/1/builds/1/status",
1572+ self.client.upload(snapbuild))
1573+ requests = [call.request for call in responses.calls]
1574+ self.assertThat(requests, MatchesListwise([
1575 RequestMatches(
1576 url=Equals("http://updown.example/unscanned-upload/"),
1577 method=Equals("POST"),
1578@@ -435,126 +420,106 @@
1579 name="binary", filename="test-snap.snap",
1580 value="dummy snap content",
1581 type="application/octet-stream",
1582- )})]))
1583- self.assertThat(self.snap_push_request, RequestMatches(
1584- url=Equals("http://sca.example/dev/api/snap-push/"),
1585- method=Equals("POST"),
1586- headers=ContainsDict({"Content-Type": Equals("application/json")}),
1587- auth=("Macaroon", MacaroonsVerify(root_key)),
1588- json_data={
1589- "name": "test-snap", "updown_id": 1, "series": "rolling",
1590- }))
1591+ )}),
1592+ RequestMatches(
1593+ url=Equals("http://sca.example/dev/api/snap-push/"),
1594+ method=Equals("POST"),
1595+ headers=ContainsDict(
1596+ {"Content-Type": Equals("application/json")}),
1597+ auth=("Macaroon", MacaroonsVerify(root_key)),
1598+ json_data={
1599+ "name": "test-snap", "updown_id": 1, "series": "rolling",
1600+ }),
1601+ ]))
1602
1603+ @responses.activate
1604 def test_upload_unauthorized(self):
1605- @urlmatch(path=r".*/snap-push/$")
1606- def snap_push_handler(url, request):
1607- self.snap_push_request = request
1608- return {
1609- "status_code": 401,
1610- "headers": {"WWW-Authenticate": 'Macaroon realm="Devportal"'},
1611- "content": {
1612- "error_list": [{
1613- "code": "macaroon-permission-required",
1614- "message": "Permission is required: package_push",
1615- }],
1616- },
1617- }
1618-
1619 store_secrets = self._make_store_secrets()
1620 snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
1621 transaction.commit()
1622+ self._addUnscannedUploadResponse()
1623+ snap_push_error = {
1624+ "code": "macaroon-permission-required",
1625+ "message": "Permission is required: package_push",
1626+ }
1627+ responses.add(
1628+ "POST", "http://sca.example/dev/api/snap-push/", status=401,
1629+ headers={"WWW-Authenticate": 'Macaroon realm="Devportal"'},
1630+ json={"error_list": [snap_push_error]})
1631 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1632- with HTTMock(self._unscanned_upload_handler, snap_push_handler,
1633- self._macaroon_refresh_handler):
1634- self.assertRaisesWithContent(
1635- UnauthorizedUploadResponse,
1636- "Permission is required: package_push",
1637- self.client.upload, snapbuild)
1638+ self.assertRaisesWithContent(
1639+ UnauthorizedUploadResponse,
1640+ "Permission is required: package_push",
1641+ self.client.upload, snapbuild)
1642
1643+ @responses.activate
1644 def test_upload_needs_discharge_macaroon_refresh(self):
1645- @urlmatch(path=r".*/snap-push/$")
1646- def snap_push_handler(url, request):
1647- snap_push_handler.call_count += 1
1648- if snap_push_handler.call_count == 1:
1649- self.first_snap_push_request = request
1650- return {
1651- "status_code": 401,
1652- "headers": {
1653- "WWW-Authenticate": "Macaroon needs_refresh=1"}}
1654- else:
1655- return self._snap_push_handler(url, request)
1656- snap_push_handler.call_count = 0
1657-
1658 store_secrets = self._make_store_secrets()
1659 snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
1660 transaction.commit()
1661+ self._addUnscannedUploadResponse()
1662+ responses.add(
1663+ "POST", "http://sca.example/dev/api/snap-push/", status=401,
1664+ headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
1665+ self._addMacaroonRefreshResponse()
1666+ self._addSnapPushResponse()
1667 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1668- with HTTMock(self._unscanned_upload_handler, snap_push_handler,
1669- self._macaroon_refresh_handler):
1670- self.assertEqual(
1671- "http://sca.example/dev/api/snaps/1/builds/1/status",
1672- self.client.upload(snapbuild))
1673- self.assertEqual(2, snap_push_handler.call_count)
1674+ self.assertEqual(
1675+ "http://sca.example/dev/api/snaps/1/builds/1/status",
1676+ self.client.upload(snapbuild))
1677+ requests = [call.request for call in responses.calls]
1678+ self.assertThat(requests, MatchesListwise([
1679+ MatchesStructure.byEquality(path_url="/unscanned-upload/"),
1680+ MatchesStructure.byEquality(path_url="/dev/api/snap-push/"),
1681+ MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
1682+ MatchesStructure.byEquality(path_url="/dev/api/snap-push/"),
1683+ ]))
1684 self.assertNotEqual(
1685 store_secrets["discharge"],
1686 snapbuild.snap.store_secrets["discharge"])
1687
1688+ @responses.activate
1689 def test_upload_unsigned_agreement(self):
1690- @urlmatch(path=r".*/snap-push/$")
1691- def snap_push_handler(url, request):
1692- self.snap_push_request = request
1693- return {
1694- "status_code": 403,
1695- "content": {
1696- "error_list": [
1697- {"message": "Developer has not signed agreement."},
1698- ],
1699- },
1700- }
1701-
1702 store_secrets = self._make_store_secrets()
1703 snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
1704 transaction.commit()
1705+ self._addUnscannedUploadResponse()
1706+ snap_push_error = {"message": "Developer has not signed agreement."}
1707+ responses.add(
1708+ "POST", "http://sca.example/dev/api/snap-push/", status=403,
1709+ json={"error_list": [snap_push_error]})
1710 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1711- with HTTMock(self._unscanned_upload_handler, snap_push_handler,
1712- self._macaroon_refresh_handler):
1713- err = self.assertRaises(
1714- UploadFailedResponse, self.client.upload, snapbuild)
1715- self.assertEqual(
1716- "Developer has not signed agreement.", str(err))
1717- self.assertFalse(err.can_retry)
1718+ err = self.assertRaises(
1719+ UploadFailedResponse, self.client.upload, snapbuild)
1720+ self.assertEqual("Developer has not signed agreement.", str(err))
1721+ self.assertFalse(err.can_retry)
1722
1723+ @responses.activate
1724 def test_upload_file_error(self):
1725- @urlmatch(path=r".*/unscanned-upload/$")
1726- def unscanned_upload_handler(url, request):
1727- return {
1728- "status_code": 502,
1729- "reason": "Proxy Error",
1730- "content": b"The proxy exploded.\n",
1731- }
1732-
1733 store_secrets = self._make_store_secrets()
1734 snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
1735 transaction.commit()
1736+ responses.add(
1737+ "POST", "http://updown.example/unscanned-upload/", status=502,
1738+ body="The proxy exploded.\n")
1739 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1740- with HTTMock(unscanned_upload_handler):
1741- err = self.assertRaises(
1742- UploadFailedResponse, self.client.upload, snapbuild)
1743- self.assertEqual("502 Server Error: Proxy Error", str(err))
1744- self.assertEqual(b"The proxy exploded.\n", err.detail)
1745- self.assertTrue(err.can_retry)
1746+ err = self.assertRaises(
1747+ UploadFailedResponse, self.client.upload, snapbuild)
1748+ self.assertEqual("502 Server Error: Bad Gateway", str(err))
1749+ self.assertEqual(b"The proxy exploded.\n", err.detail)
1750+ self.assertTrue(err.can_retry)
1751
1752+ @responses.activate
1753 def test_refresh_discharge_macaroon(self):
1754 store_secrets = self._make_store_secrets()
1755 snap = self.factory.makeSnap(
1756 store_upload=True,
1757 store_series=self.factory.makeSnappySeries(name="rolling"),
1758 store_name="test-snap", store_secrets=store_secrets)
1759-
1760+ self._addMacaroonRefreshResponse()
1761 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1762- with HTTMock(self._macaroon_refresh_handler):
1763- self.client.refreshDischargeMacaroon(snap)
1764- self.assertThat(self.refresh_request, RequestMatches(
1765+ self.client.refreshDischargeMacaroon(snap)
1766+ self.assertThat(responses.calls[-1].request, RequestMatches(
1767 url=Equals("http://sso.example/api/v2/tokens/refresh"),
1768 method=Equals("POST"),
1769 headers=ContainsDict({"Content-Type": Equals("application/json")}),
1770@@ -562,127 +527,100 @@
1771 self.assertNotEqual(
1772 store_secrets["discharge"], snap.store_secrets["discharge"])
1773
1774+ @responses.activate
1775 def test_checkStatus_pending(self):
1776- @all_requests
1777- def handler(url, request):
1778- return {
1779- "status_code": 200,
1780- "content": {
1781- "code": "being_processed", "processed": False,
1782- "can_release": False,
1783- }}
1784-
1785 status_url = "http://sca.example/dev/api/snaps/1/builds/1/status"
1786- with HTTMock(handler):
1787- self.assertRaises(
1788- UploadNotScannedYetResponse, self.client.checkStatus,
1789- status_url)
1790+ responses.add(
1791+ "GET", status_url,
1792+ json={
1793+ "code": "being_processed", "processed": False,
1794+ "can_release": False,
1795+ })
1796+ self.assertRaises(
1797+ UploadNotScannedYetResponse, self.client.checkStatus, status_url)
1798
1799+ @responses.activate
1800 def test_checkStatus_error(self):
1801- @all_requests
1802- def handler(url, request):
1803- return {
1804- "status_code": 200,
1805- "content": {
1806- "code": "processing_error", "processed": True,
1807- "can_release": False,
1808- "errors": [
1809- {"code": None,
1810- "message": "You cannot use that reserved namespace.",
1811- "link": "http://example.com"
1812- }],
1813- }}
1814-
1815 status_url = "http://sca.example/dev/api/snaps/1/builds/1/status"
1816- with HTTMock(handler):
1817- self.assertRaisesWithContent(
1818- ScanFailedResponse,
1819- b"You cannot use that reserved namespace.",
1820- self.client.checkStatus, status_url)
1821+ responses.add(
1822+ "GET", status_url,
1823+ json={
1824+ "code": "processing_error", "processed": True,
1825+ "can_release": False,
1826+ "errors": [
1827+ {"code": None,
1828+ "message": "You cannot use that reserved namespace.",
1829+ "link": "http://example.com"
1830+ }],
1831+ })
1832+ self.assertRaisesWithContent(
1833+ ScanFailedResponse, b"You cannot use that reserved namespace.",
1834+ self.client.checkStatus, status_url)
1835
1836+ @responses.activate
1837 def test_checkStatus_review_error(self):
1838- @all_requests
1839- def handler(url, request):
1840- return {
1841- "status_code": 200,
1842- "content": {
1843- "code": "processing_error", "processed": True,
1844- "can_release": False,
1845- "errors": [{"code": None, "message": "Review failed."}],
1846- "url": "http://sca.example/dev/click-apps/1/rev/1/",
1847- }}
1848-
1849 status_url = "http://sca.example/dev/api/snaps/1/builds/1/status"
1850- with HTTMock(handler):
1851- self.assertRaisesWithContent(
1852- ScanFailedResponse, b"Review failed.",
1853- self.client.checkStatus, status_url)
1854+ responses.add(
1855+ "GET", status_url,
1856+ json={
1857+ "code": "processing_error", "processed": True,
1858+ "can_release": False,
1859+ "errors": [{"code": None, "message": "Review failed."}],
1860+ "url": "http://sca.example/dev/click-apps/1/rev/1/",
1861+ })
1862+ self.assertRaisesWithContent(
1863+ ScanFailedResponse, b"Review failed.",
1864+ self.client.checkStatus, status_url)
1865
1866+ @responses.activate
1867 def test_checkStatus_complete(self):
1868- @all_requests
1869- def handler(url, request):
1870- return {
1871- "status_code": 200,
1872- "content": {
1873- "code": "ready_to_release", "processed": True,
1874- "can_release": True,
1875- "url": "http://sca.example/dev/click-apps/1/rev/1/",
1876- "revision": 1,
1877- }}
1878-
1879 status_url = "http://sca.example/dev/api/snaps/1/builds/1/status"
1880- with HTTMock(handler):
1881- self.assertEqual(
1882- ("http://sca.example/dev/click-apps/1/rev/1/", 1),
1883- self.client.checkStatus(status_url))
1884+ responses.add(
1885+ "GET", status_url,
1886+ json={
1887+ "code": "ready_to_release", "processed": True,
1888+ "can_release": True,
1889+ "url": "http://sca.example/dev/click-apps/1/rev/1/",
1890+ "revision": 1,
1891+ })
1892+ self.assertEqual(
1893+ ("http://sca.example/dev/click-apps/1/rev/1/", 1),
1894+ self.client.checkStatus(status_url))
1895
1896+ @responses.activate
1897 def test_checkStatus_404(self):
1898- @all_requests
1899- def handler(url, request):
1900- return {"status_code": 404, "reason": b"Not found"}
1901-
1902 status_url = "http://sca.example/dev/api/snaps/1/builds/1/status"
1903- with HTTMock(handler):
1904- self.assertRaisesWithContent(
1905- BadScanStatusResponse, b"404 Client Error: Not found",
1906- self.client.checkStatus, status_url)
1907+ responses.add("GET", status_url, status=404)
1908+ self.assertRaisesWithContent(
1909+ BadScanStatusResponse, b"404 Client Error: Not Found",
1910+ self.client.checkStatus, status_url)
1911
1912+ @responses.activate
1913 def test_listChannels(self):
1914- memcache_key = "search.example:channels".encode("UTF-8")
1915- try:
1916- with HTTMock(self._channels_handler):
1917- self.assertEqual(self.channels, self.client.listChannels())
1918- self.assertThat(self.channels_request, RequestMatches(
1919- url=Equals("http://search.example/api/v1/channels"),
1920- method=Equals("GET"),
1921- headers=ContainsDict(
1922- {"Accept": Equals("application/hal+json")})))
1923- self.assertEqual(
1924- self.channels,
1925- json.loads(getUtility(IMemcacheClient).get(memcache_key)))
1926- self.channels_request = None
1927- with HTTMock(self._channels_handler):
1928- self.assertEqual(self.channels, self.client.listChannels())
1929- self.assertIsNone(self.channels_request)
1930- finally:
1931- getUtility(IMemcacheClient).delete(memcache_key)
1932+ self._addChannelsResponse()
1933+ self.assertEqual(self.channels, self.client.listChannels())
1934+ self.assertThat(responses.calls[-1].request, RequestMatches(
1935+ url=Equals("http://search.example/api/v1/channels"),
1936+ method=Equals("GET"),
1937+ headers=ContainsDict({"Accept": Equals("application/hal+json")})))
1938+ self.assertEqual(
1939+ self.channels,
1940+ json.loads(getUtility(IMemcacheClient).get(
1941+ self.channels_memcache_key)))
1942+ responses.reset()
1943+ self.assertEqual(self.channels, self.client.listChannels())
1944+ self.assertContentEqual([], responses.calls)
1945
1946+ @responses.activate
1947 def test_listChannels_404(self):
1948- @all_requests
1949- def handler(url, request):
1950- return {"status_code": 404, "reason": b"Not found"}
1951-
1952- with HTTMock(handler):
1953- self.assertRaisesWithContent(
1954- BadSearchResponse, b"404 Client Error: Not found",
1955- self.client.listChannels)
1956-
1957+ responses.add(
1958+ "GET", "http://search.example/api/v1/channels", status=404)
1959+ self.assertRaisesWithContent(
1960+ BadSearchResponse, b"404 Client Error: Not Found",
1961+ self.client.listChannels)
1962+
1963+ @responses.activate
1964 def test_listChannels_disable_search(self):
1965- @all_requests
1966- def handler(url, request):
1967- self.request = request
1968- return {"status_code": 404, "reason": b"Not found"}
1969-
1970 self.useFixture(
1971 FeatureFixture({u"snap.disable_channel_search": u"on"}))
1972 expected_channels = [
1973@@ -691,25 +629,23 @@
1974 {"name": "beta", "display_name": "Beta"},
1975 {"name": "stable", "display_name": "Stable"},
1976 ]
1977- self.request = None
1978- with HTTMock(handler):
1979- self.assertEqual(expected_channels, self.client.listChannels())
1980- self.assertIsNone(self.request)
1981- memcache_key = "search.example:channels".encode("UTF-8")
1982- self.assertIsNone(getUtility(IMemcacheClient).get(memcache_key))
1983+ self.assertEqual(expected_channels, self.client.listChannels())
1984+ self.assertContentEqual([], responses.calls)
1985+ self.assertIsNone(
1986+ getUtility(IMemcacheClient).get(self.channels_memcache_key))
1987
1988+ @responses.activate
1989 def test_release(self):
1990- with HTTMock(self._channels_handler):
1991- snap = self.factory.makeSnap(
1992- store_upload=True,
1993- store_series=self.factory.makeSnappySeries(name="rolling"),
1994- store_name="test-snap",
1995- store_secrets=self._make_store_secrets(),
1996- store_channels=["stable", "edge"])
1997+ snap = self.factory.makeSnap(
1998+ store_upload=True,
1999+ store_series=self.factory.makeSnappySeries(name="rolling"),
2000+ store_name="test-snap",
2001+ store_secrets=self._make_store_secrets(),
2002+ store_channels=["stable", "edge"])
2003 snapbuild = self.factory.makeSnapBuild(snap=snap)
2004- with HTTMock(self._snap_release_handler):
2005- self.client.release(snapbuild, 1)
2006- self.assertThat(self.snap_release_request, RequestMatches(
2007+ self._addSnapReleaseResponse()
2008+ self.client.release(snapbuild, 1)
2009+ self.assertThat(responses.calls[-1].request, RequestMatches(
2010 url=Equals("http://sca.example/dev/api/snap-release/"),
2011 method=Equals("POST"),
2012 headers=ContainsDict({"Content-Type": Equals("application/json")}),
2013@@ -719,20 +655,20 @@
2014 "channels": ["stable", "edge"], "series": "rolling",
2015 }))
2016
2017+ @responses.activate
2018 def test_release_no_discharge(self):
2019 root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
2020 root_macaroon = Macaroon(key=root_key)
2021- with HTTMock(self._channels_handler):
2022- snap = self.factory.makeSnap(
2023- store_upload=True,
2024- store_series=self.factory.makeSnappySeries(name="rolling"),
2025- store_name="test-snap",
2026- store_secrets={"root": root_macaroon.serialize()},
2027- store_channels=["stable", "edge"])
2028+ snap = self.factory.makeSnap(
2029+ store_upload=True,
2030+ store_series=self.factory.makeSnappySeries(name="rolling"),
2031+ store_name="test-snap",
2032+ store_secrets={"root": root_macaroon.serialize()},
2033+ store_channels=["stable", "edge"])
2034 snapbuild = self.factory.makeSnapBuild(snap=snap)
2035- with HTTMock(self._snap_release_handler):
2036- self.client.release(snapbuild, 1)
2037- self.assertThat(self.snap_release_request, RequestMatches(
2038+ self._addSnapReleaseResponse()
2039+ self.client.release(snapbuild, 1)
2040+ self.assertThat(responses.calls[-1].request, RequestMatches(
2041 url=Equals("http://sca.example/dev/api/snap-release/"),
2042 method=Equals("POST"),
2043 headers=ContainsDict({"Content-Type": Equals("application/json")}),
2044@@ -742,69 +678,55 @@
2045 "channels": ["stable", "edge"], "series": "rolling",
2046 }))
2047
2048+ @responses.activate
2049 def test_release_needs_discharge_macaroon_refresh(self):
2050- @urlmatch(path=r".*/snap-release/$")
2051- def snap_release_handler(url, request):
2052- snap_release_handler.call_count += 1
2053- if snap_release_handler.call_count == 1:
2054- self.first_snap_release_request = request
2055- return {
2056- "status_code": 401,
2057- "headers": {
2058- "WWW-Authenticate": "Macaroon needs_refresh=1"}}
2059- else:
2060- return self._snap_release_handler(url, request)
2061- snap_release_handler.call_count = 0
2062-
2063 store_secrets = self._make_store_secrets()
2064- with HTTMock(self._channels_handler):
2065- snap = self.factory.makeSnap(
2066- store_upload=True,
2067- store_series=self.factory.makeSnappySeries(name="rolling"),
2068- store_name="test-snap", store_secrets=store_secrets,
2069- store_channels=["stable", "edge"])
2070+ snap = self.factory.makeSnap(
2071+ store_upload=True,
2072+ store_series=self.factory.makeSnappySeries(name="rolling"),
2073+ store_name="test-snap", store_secrets=store_secrets,
2074+ store_channels=["stable", "edge"])
2075 snapbuild = self.factory.makeSnapBuild(snap=snap)
2076- with HTTMock(snap_release_handler, self._macaroon_refresh_handler):
2077- self.client.release(snapbuild, 1)
2078- self.assertEqual(2, snap_release_handler.call_count)
2079+ responses.add(
2080+ "POST", "http://sca.example/dev/api/snap-release/", status=401,
2081+ headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
2082+ self._addMacaroonRefreshResponse()
2083+ self._addSnapReleaseResponse()
2084+ self.client.release(snapbuild, 1)
2085+ requests = [call.request for call in responses.calls]
2086+ self.assertThat(requests, MatchesListwise([
2087+ MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
2088+ MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
2089+ MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
2090+ ]))
2091 self.assertNotEqual(
2092 store_secrets["discharge"], snap.store_secrets["discharge"])
2093
2094+ @responses.activate
2095 def test_release_error(self):
2096- @urlmatch(path=r".*/snap-release/$")
2097- def handler(url, request):
2098- return {
2099- "status_code": 503,
2100- "content": {"error_list": [{"message": "Failed to publish"}]},
2101- }
2102-
2103- with HTTMock(self._channels_handler):
2104- snap = self.factory.makeSnap(
2105- store_upload=True,
2106- store_series=self.factory.makeSnappySeries(name="rolling"),
2107- store_name="test-snap",
2108- store_secrets=self._make_store_secrets(),
2109- store_channels=["stable", "edge"])
2110+ snap = self.factory.makeSnap(
2111+ store_upload=True,
2112+ store_series=self.factory.makeSnappySeries(name="rolling"),
2113+ store_name="test-snap", store_secrets=self._make_store_secrets(),
2114+ store_channels=["stable", "edge"])
2115 snapbuild = self.factory.makeSnapBuild(snap=snap)
2116- with HTTMock(handler):
2117- self.assertRaisesWithContent(
2118- ReleaseFailedResponse, "Failed to publish",
2119- self.client.release, snapbuild, 1)
2120+ responses.add(
2121+ "POST", "http://sca.example/dev/api/snap-release/", status=503,
2122+ json={"error_list": [{"message": "Failed to publish"}]})
2123+ self.assertRaisesWithContent(
2124+ ReleaseFailedResponse, "Failed to publish",
2125+ self.client.release, snapbuild, 1)
2126
2127+ @responses.activate
2128 def test_release_404(self):
2129- @urlmatch(path=r".*/snap-release/$")
2130- def handler(url, request):
2131- return {"status_code": 404, "reason": b"Not found"}
2132-
2133- with HTTMock(self._channels_handler):
2134- snap = self.factory.makeSnap(
2135- store_upload=True,
2136- store_series=self.factory.makeSnappySeries(name="rolling"),
2137- store_name="test-snap",
2138- store_secrets=self._make_store_secrets(),
2139- store_channels=["stable", "edge"])
2140+ snap = self.factory.makeSnap(
2141+ store_upload=True,
2142+ store_series=self.factory.makeSnappySeries(name="rolling"),
2143+ store_name="test-snap", store_secrets=self._make_store_secrets(),
2144+ store_channels=["stable", "edge"])
2145 snapbuild = self.factory.makeSnapBuild(snap=snap)
2146- with HTTMock(handler):
2147- self.assertRaisesWithContent(
2148- ReleaseFailedResponse, b"404 Client Error: Not found",
2149- self.client.release, snapbuild, 1)
2150+ responses.add(
2151+ "POST", "http://sca.example/dev/api/snap-release/", status=404)
2152+ self.assertRaisesWithContent(
2153+ ReleaseFailedResponse, b"404 Client Error: Not Found",
2154+ self.client.release, snapbuild, 1)
2155
2156=== modified file 'setup.py'
2157--- setup.py 2018-05-21 20:30:16 +0000
2158+++ setup.py 2018-05-31 10:48:11 +0000
2159@@ -160,7 +160,6 @@
2160 'feedvalidator',
2161 'fixtures',
2162 'html5browser',
2163- 'httmock',
2164 'importlib-resources',
2165 'ipython',
2166 'jsautobuild',
2167@@ -209,6 +208,7 @@
2168 'rabbitfixture',
2169 'requests',
2170 'requests-toolbelt',
2171+ 'responses',
2172 'scandir',
2173 'setproctitle',
2174 'setuptools',