Merge lp:~cjwatson/launchpad/explicit-proxy-trac into lp:launchpad
- explicit-proxy-trac
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18701 |
Proposed branch: | lp:~cjwatson/launchpad/explicit-proxy-trac |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/explicit-proxy-bugzilla |
Diff against target: |
885 lines (+221/-226) 5 files modified
lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt (+70/-71) lib/lp/bugs/doc/externalbugtracker-trac.txt (+67/-86) lib/lp/bugs/externalbugtracker/trac.py (+46/-34) lib/lp/bugs/tests/externalbugtracker.py (+37/-35) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/explicit-proxy-trac |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+348433@code.launchpad.net |
Commit message
Convert the Trac external bug tracker to urlfetch.
Description of the change
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 'lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt' |
2 | --- lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt 2016-09-21 02:49:42 +0000 |
3 | +++ lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt 2018-06-23 02:06:14 +0000 |
4 | @@ -30,23 +30,22 @@ |
5 | Trac to validate $token and return a Set-Cookie header. |
6 | |
7 | >>> import random |
8 | + >>> import re |
9 | + >>> from six.moves.urllib_parse import ( |
10 | + ... urljoin, |
11 | + ... urlsplit, |
12 | + ... ) |
13 | + >>> from lp.bugs.tests.externalbugtracker import BugTrackerResponsesMixin |
14 | >>> from lp.services.verification.interfaces.logintoken import ( |
15 | ... ILoginTokenSet) |
16 | - >>> from lp.services.webapp.url import urlappend |
17 | >>> from lp.testing.dbuser import lp_dbuser |
18 | |
19 | - >>> class FakeResponse: |
20 | - ... def __init__(self): |
21 | - ... self.headers = {} |
22 | - |
23 | - >>> class TestTracLPPlugin(TracLPPlugin): |
24 | - ... def urlopen(self, url, data=None): |
25 | + >>> class TestTracLPPlugin(BugTrackerResponsesMixin, TracLPPlugin): |
26 | + ... def _getCallback(self, request): |
27 | ... with lp_dbuser(): |
28 | - ... url = url.get_full_url() |
29 | - ... base_auth_url = urlappend(self.baseurl, 'launchpad-auth') |
30 | - ... if not url.startswith(base_auth_url + '/'): |
31 | - ... raise AssertionError("Unexpected URL: %s" % url) |
32 | - ... token_text = url.split('/')[-1] |
33 | + ... login(ANONYMOUS) |
34 | + ... url = urlsplit(request.url) |
35 | + ... token_text = url.path.split('/')[-1] |
36 | ... token = getUtility(ILoginTokenSet)[token_text] |
37 | ... if token.tokentype.name != 'BUGTRACKER': |
38 | ... raise AssertionError( |
39 | @@ -59,20 +58,24 @@ |
40 | ... cookie_string = ( |
41 | ... 'trac_auth=random_token-' + str(random.random())) |
42 | ... self._xmlrpc_transport.setCookie(cookie_string) |
43 | - ... response = FakeResponse() |
44 | - ... response.headers['Set-Cookie'] = cookie_string |
45 | + ... return 200, {'Set-Cookie': cookie_string}, '' |
46 | ... |
47 | - ... return response |
48 | + ... def addResponses(self, requests_mock): |
49 | + ... requests_mock.add_callback( |
50 | + ... 'GET', |
51 | + ... re.compile( |
52 | + ... re.escape(urljoin(self.baseurl, 'launchpad-auth/'))), |
53 | + ... self._getCallback) |
54 | |
55 | To generate the token, the internal XML-RPC server is used. By using the |
56 | -XML-RPC server rather than talking to the database directy means that we |
57 | -don't have to bother about commiting the transaction to make the token |
58 | +XML-RPC server rather than talking to the database directly means that we |
59 | +don't have to bother about committing the transaction to make the token |
60 | visible to Trac. |
61 | |
62 | - >>> from cookielib import CookieJar |
63 | + >>> from requests.cookies import RequestsCookieJar |
64 | >>> from lp.bugs.tests.externalbugtracker import ( |
65 | ... TestInternalXMLRPCTransport) |
66 | - >>> cookie_jar = CookieJar() |
67 | + >>> cookie_jar = RequestsCookieJar() |
68 | >>> test_transport = TestTracXMLRPCTransport( |
69 | ... 'http://example.com/', cookie_jar) |
70 | >>> trac = TestTracLPPlugin( |
71 | @@ -82,70 +85,57 @@ |
72 | |
73 | The method that authenticates with Trac is _authenticate(). |
74 | |
75 | - >>> trac._authenticate() |
76 | + >>> with trac.responses(): |
77 | + ... trac._authenticate() |
78 | Using XML-RPC to generate token. |
79 | Successfully validated the token. |
80 | |
81 | After it has been called, the XML-RPC transport will have its |
82 | auth_cookie attribute set. |
83 | |
84 | - >>> test_transport.cookie_processor.cookiejar |
85 | - <...CookieJar[Cookie(version=0, name='trac_auth'... |
86 | - |
87 | -The XML-RPC transport's cookie_processor shares its cookiejar with the |
88 | -TracLPPlugin instance. This is so that the TracLPPlugin can use the |
89 | -cookiejar when authenticating with the remote Trac and then pass it to |
90 | -the XML-RPC transport for further use, meaning that there's no need to |
91 | -manually manipulate cookies. |
92 | - |
93 | - >>> test_transport.cookie_processor.cookiejar == trac._cookie_jar |
94 | + >>> test_transport.cookie_jar |
95 | + <RequestsCookieJar[Cookie(version=0, name='trac_auth'... |
96 | + |
97 | +The XML-RPC transport shares its cookiejar with the TracLPPlugin instance. |
98 | +This is so that the TracLPPlugin can use the cookiejar when authenticating |
99 | +with the remote Trac and then pass it to the XML-RPC transport for further |
100 | +use, meaning that there's no need to manually manipulate cookies. |
101 | + |
102 | + >>> test_transport.cookie_jar == trac._cookie_jar |
103 | True |
104 | |
105 | So if we look in the TracLPPlugin's CookieJar we'll see the same cookie: |
106 | |
107 | >>> trac._cookie_jar |
108 | - <...CookieJar[Cookie(version=0, name='trac_auth'... |
109 | + <RequestsCookieJar[Cookie(version=0, name='trac_auth'... |
110 | |
111 | And altering the cookie in the TracLPPlugin's CookieJar will mean, of |
112 | course, that it's altered in the XML-RPC transport's CookieJar, too. |
113 | |
114 | - >>> from cookielib import Cookie |
115 | - >>> new_cookie = Cookie( |
116 | - ... name="trac_auth", |
117 | - ... value="Look ma, a new cookie!", |
118 | - ... version=0, port=None, port_specified=False, |
119 | - ... domain='http://example.com', domain_specified=True, |
120 | - ... domain_initial_dot=None, path='', path_specified=False, |
121 | - ... secure=False, expires=False, discard=None, comment=None, |
122 | - ... comment_url=None, rest=None) |
123 | - |
124 | >>> trac._cookie_jar.clear() |
125 | - >>> trac._cookie_jar.set_cookie(new_cookie) |
126 | + >>> _ = trac._cookie_jar.set( |
127 | + ... 'trac_auth', 'Look ma, a new cookie!', |
128 | + ... domain='http://example.com', path='') |
129 | |
130 | >>> trac._cookie_jar |
131 | <...CookieJar[Cookie(version=0, name='trac_auth', |
132 | value='Look ma, a new cookie!'...> |
133 | |
134 | - >>> test_transport.cookie_processor.cookiejar |
135 | + >>> test_transport.cookie_jar |
136 | <...CookieJar[Cookie(version=0, name='trac_auth', |
137 | value='Look ma, a new cookie!'...> |
138 | |
139 | If authentication fails, a BugTrackerAuthenticationError will be raised. |
140 | |
141 | - >>> from urllib2 import HTTPError |
142 | - >>> class TestFailingTracLPPlugin(TracLPPlugin): |
143 | - ... def urlopen(self, url, data=None): |
144 | - ... raise HTTPError(url, 401, "Denied!", {}, None) |
145 | - |
146 | - >>> test_trac = TestFailingTracLPPlugin( |
147 | - ... 'http://example.com', xmlrpc_transport=test_transport, |
148 | - ... internal_xmlrpc_transport=TestInternalXMLRPCTransport(), |
149 | - ... cookie_jar=cookie_jar) |
150 | - >>> test_trac._authenticate() |
151 | + >>> with trac.responses() as requests_mock: |
152 | + ... requests_mock.reset() |
153 | + ... requests_mock.add( |
154 | + ... 'GET', re.compile(r'.*/launchpad-auth/.*'), status=401) |
155 | + ... trac._authenticate() |
156 | Traceback (most recent call last): |
157 | ... |
158 | BugTrackerAuthenticationError: http://example.com: |
159 | - HTTP Error 401: Denied! |
160 | + 401 Client Error: Unauthorized |
161 | |
162 | |
163 | Current time |
164 | @@ -169,7 +159,8 @@ |
165 | >>> test_transport.seconds_since_epoch = 1207706521 + HOUR |
166 | >>> test_transport.local_timezone = 'CET' |
167 | >>> test_transport.utc_offset = HOUR |
168 | - >>> trac.getCurrentDBTime() |
169 | + >>> with trac.responses(): |
170 | + ... trac.getCurrentDBTime() |
171 | Using XML-RPC to generate token. |
172 | Successfully validated the token. |
173 | datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>) |
174 | @@ -187,7 +178,8 @@ |
175 | sent again. |
176 | |
177 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
178 | - >>> trac.getCurrentDBTime() |
179 | + >>> with trac.responses(): |
180 | + ... trac.getCurrentDBTime() |
181 | Using XML-RPC to generate token. |
182 | Successfully validated the token. |
183 | datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>) |
184 | @@ -220,8 +212,8 @@ |
185 | >>> bug_ids_to_check = ['1', '2', '3'] |
186 | >>> last_checked = datetime(2008, 1, 1, 0, 0, 0) |
187 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
188 | - >>> sorted(trac.getModifiedRemoteBugs( |
189 | - ... bug_ids_to_check, last_checked)) |
190 | + >>> with trac.responses(): |
191 | + ... sorted(trac.getModifiedRemoteBugs(bug_ids_to_check, last_checked)) |
192 | Using XML-RPC to generate token. |
193 | Successfully validated the token. |
194 | ['1', '3'] |
195 | @@ -231,8 +223,8 @@ |
196 | |
197 | >>> last_checked = datetime(2008, 2, 1, 0, 0, 0) |
198 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
199 | - >>> trac.getModifiedRemoteBugs( |
200 | - ... bug_ids_to_check, last_checked) |
201 | + >>> with trac.responses(): |
202 | + ... trac.getModifiedRemoteBugs(bug_ids_to_check, last_checked) |
203 | Using XML-RPC to generate token. |
204 | Successfully validated the token. |
205 | ['1'] |
206 | @@ -242,8 +234,8 @@ |
207 | |
208 | >>> last_checked = datetime(2008, 5, 1, 0, 0, 0) |
209 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
210 | - >>> trac.getModifiedRemoteBugs( |
211 | - ... bug_ids_to_check, last_checked) |
212 | + >>> with trac.responses(): |
213 | + ... trac.getModifiedRemoteBugs(bug_ids_to_check, last_checked) |
214 | Using XML-RPC to generate token. |
215 | Successfully validated the token. |
216 | [] |
217 | @@ -255,8 +247,8 @@ |
218 | >>> bug_ids_to_check = ['1', '2', '3', '99', '100'] |
219 | >>> last_checked = datetime(2008, 1, 1, 0, 0, 0) |
220 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
221 | - >>> sorted(trac.getModifiedRemoteBugs( |
222 | - ... bug_ids_to_check, last_checked)) |
223 | + >>> with trac.responses(): |
224 | + ... sorted(trac.getModifiedRemoteBugs(bug_ids_to_check, last_checked)) |
225 | Using XML-RPC to generate token. |
226 | Successfully validated the token. |
227 | ['1', '100', '3', '99'] |
228 | @@ -281,7 +273,8 @@ |
229 | >>> bugs_to_update = trac.getModifiedRemoteBugs( |
230 | ... bug_ids_to_check, last_checked) |
231 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
232 | - >>> trac.initializeRemoteBugDB(bugs_to_update) |
233 | + >>> with trac.responses(): |
234 | + ... trac.initializeRemoteBugDB(bugs_to_update) |
235 | Using XML-RPC to generate token. |
236 | Successfully validated the token. |
237 | |
238 | @@ -368,7 +361,8 @@ |
239 | |
240 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
241 | >>> bugs_to_update = ['1', '2', '3'] |
242 | - >>> trac.initializeRemoteBugDB(bugs_to_update) |
243 | + >>> with trac.responses(): |
244 | + ... trac.initializeRemoteBugDB(bugs_to_update) |
245 | Using XML-RPC to generate token. |
246 | Successfully validated the token. |
247 | |
248 | @@ -410,7 +404,8 @@ |
249 | >>> transaction.commit() |
250 | |
251 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
252 | - >>> trac.fetchComments(remote_bug, ['1-1']) |
253 | + >>> with trac.responses(): |
254 | + ... trac.fetchComments(remote_bug, ['1-1']) |
255 | Using XML-RPC to generate token. |
256 | Successfully validated the token. |
257 | |
258 | @@ -512,8 +507,9 @@ |
259 | >>> message_rfc822msgid = message.rfc822msgid |
260 | >>> transaction.commit() |
261 | |
262 | - >>> remote_comment_id = trac.addRemoteComment( |
263 | - ... '3', message_text_contents, message_rfc822msgid) |
264 | + >>> with trac.responses(): |
265 | + ... remote_comment_id = trac.addRemoteComment( |
266 | + ... '3', message_text_contents, message_rfc822msgid) |
267 | Using XML-RPC to generate token. |
268 | Successfully validated the token. |
269 | |
270 | @@ -553,7 +549,8 @@ |
271 | getLaunchpadBugId() requires authentication. |
272 | |
273 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
274 | - >>> launchpad_bug_id = trac.getLaunchpadBugId('3') |
275 | + >>> with trac.responses(): |
276 | + ... launchpad_bug_id = trac.getLaunchpadBugId('3') |
277 | Using XML-RPC to generate token. |
278 | Successfully validated the token. |
279 | |
280 | @@ -564,7 +561,9 @@ |
281 | bug. setLaunchpadBugId() also requires authentication. |
282 | |
283 | >>> test_transport.expireCookie(test_transport.auth_cookie) |
284 | - >>> trac.setLaunchpadBugId('3', 15, 'http://bugs.launchpad.dev/bugs/xxx') |
285 | + >>> with trac.responses(): |
286 | + ... trac.setLaunchpadBugId( |
287 | + ... '3', 15, 'http://bugs.launchpad.dev/bugs/xxx') |
288 | Using XML-RPC to generate token. |
289 | Successfully validated the token. |
290 | |
291 | |
292 | === modified file 'lib/lp/bugs/doc/externalbugtracker-trac.txt' |
293 | --- lib/lp/bugs/doc/externalbugtracker-trac.txt 2016-07-04 17:08:29 +0000 |
294 | +++ lib/lp/bugs/doc/externalbugtracker-trac.txt 2018-06-23 02:06:14 +0000 |
295 | @@ -31,17 +31,14 @@ |
296 | If the LP plugin is installed, the URL will return 401, since it fails |
297 | to validate the token. |
298 | |
299 | - >>> import urllib2 |
300 | - >>> class TracHavingLPPlugin401(Trac): |
301 | - ... def urlopen(self, url, data=None): |
302 | - ... print url |
303 | - ... raise urllib2.HTTPError( |
304 | - ... url, 401, "Unauthorized", None, None) |
305 | - >>> trac = TracHavingLPPlugin401('http://example.com/') |
306 | - >>> chosen_trac = trac.getExternalBugTrackerToUse() |
307 | - http://example.com/launchpad-auth/check |
308 | - |
309 | + >>> import responses |
310 | >>> from lp.bugs.externalbugtracker.trac import TracLPPlugin |
311 | + |
312 | + >>> trac = Trac('http://example.com/') |
313 | + >>> with responses.RequestsMock() as requests_mock: |
314 | + ... requests_mock.add( |
315 | + ... 'GET', 'http://example.com/launchpad-auth/check', status=401) |
316 | + ... chosen_trac = trac.getExternalBugTrackerToUse() |
317 | >>> isinstance(chosen_trac, TracLPPlugin) |
318 | True |
319 | >>> chosen_trac.baseurl |
320 | @@ -54,30 +51,13 @@ |
321 | a valid token, which is very unlikely), is that the broken Trac will |
322 | not include a "trac_auth" cookie. |
323 | |
324 | - >>> from StringIO import StringIO |
325 | - >>> from httplib import HTTPMessage |
326 | - >>> from urllib import addinfourl |
327 | - |
328 | - >>> class TracReturning200ForPageNotFound(Trac): |
329 | - ... def urlopen(self, url, data=None): |
330 | - ... print url |
331 | - ... return addinfourl( |
332 | - ... StringIO(''), HTTPMessage(StringIO('')), url) |
333 | - |
334 | - >>> class TracHavingLPPlugin200(Trac): |
335 | - ... def urlopen(self, url, data=None): |
336 | - ... print url |
337 | - ... return addinfourl( |
338 | - ... StringIO(''), HTTPMessage( |
339 | - ... StringIO('Set-Cookie: trac_auth=1234')), url) |
340 | - |
341 | The plain, non-plugin, external bug tracker is selected for broken |
342 | Trac installations: |
343 | |
344 | - >>> trac = TracReturning200ForPageNotFound('http://example.com/') |
345 | - >>> chosen_trac = trac.getExternalBugTrackerToUse() |
346 | - http://example.com/launchpad-auth/check |
347 | - |
348 | + >>> with responses.RequestsMock() as requests_mock: |
349 | + ... requests_mock.add( |
350 | + ... 'GET', 'http://example.com/launchpad-auth/check') |
351 | + ... chosen_trac = trac.getExternalBugTrackerToUse() |
352 | >>> isinstance(chosen_trac, TracLPPlugin) |
353 | False |
354 | >>> chosen_trac.baseurl |
355 | @@ -86,10 +66,11 @@ |
356 | In the event that our deliberately bogus token is considered valid, |
357 | the external bug tracker that groks the plugin is selected: |
358 | |
359 | - >>> trac = TracHavingLPPlugin200('http://example.com/') |
360 | - >>> chosen_trac = trac.getExternalBugTrackerToUse() |
361 | - http://example.com/launchpad-auth/check |
362 | - |
363 | + >>> with responses.RequestsMock() as requests_mock: |
364 | + ... requests_mock.add( |
365 | + ... 'GET', 'http://example.com/launchpad-auth/check', |
366 | + ... headers={'Set-Cookie': 'trac_auth=1234'}) |
367 | + ... chosen_trac = trac.getExternalBugTrackerToUse() |
368 | >>> isinstance(chosen_trac, TracLPPlugin) |
369 | True |
370 | >>> chosen_trac.baseurl |
371 | @@ -97,15 +78,10 @@ |
372 | |
373 | If a 404 is returned, the normal Trac instance is returned. |
374 | |
375 | - >>> class TracNotHavingLPPlugin(Trac): |
376 | - ... def urlopen(self, url, data=None): |
377 | - ... print url |
378 | - ... raise urllib2.HTTPError( |
379 | - ... url, 404, "Not found", None, None) |
380 | - >>> trac = TracNotHavingLPPlugin('http://example.com/') |
381 | - >>> chosen_trac = trac.getExternalBugTrackerToUse() |
382 | - http://example.com/launchpad-auth/check |
383 | - |
384 | + >>> with responses.RequestsMock() as requests_mock: |
385 | + ... requests_mock.add( |
386 | + ... 'GET', 'http://example.com/launchpad-auth/check', status=404) |
387 | + ... chosen_trac = trac.getExternalBugTrackerToUse() |
388 | >>> chosen_trac is trac |
389 | True |
390 | |
391 | @@ -113,14 +89,12 @@ |
392 | instance. It will deal with the connection error later, if the situation |
393 | persists. |
394 | |
395 | - >>> class TracWithConnectionError(Trac): |
396 | - ... def urlopen(self, url, data=None): |
397 | - ... print url |
398 | - ... raise urllib2.URLError("Connection timed out") |
399 | - >>> trac = TracWithConnectionError('http://example.com/') |
400 | - >>> chosen_trac = trac.getExternalBugTrackerToUse() |
401 | - http://example.com/launchpad-auth/check |
402 | - |
403 | + >>> from requests.exceptions import ConnectTimeout |
404 | + >>> with responses.RequestsMock() as requests_mock: |
405 | + ... requests_mock.add( |
406 | + ... 'GET', 'http://example.com/launchpad-auth/check', |
407 | + ... body=ConnectTimeout()) |
408 | + ... chosen_trac = trac.getExternalBugTrackerToUse() |
409 | >>> chosen_trac is trac |
410 | True |
411 | |
412 | @@ -172,18 +146,19 @@ |
413 | local variable for later use. |
414 | |
415 | We use a test-oriented implementation for the purposes of these tests, which |
416 | -overrides ExternalBugTracker.urlopen() so that we don't have to rely on a |
417 | -working network connection. |
418 | +avoids relying on a network connection. |
419 | |
420 | >>> from lp.bugs.tests.externalbugtracker import TestTrac |
421 | >>> trac = TestTrac(u'http://test.trac/') |
422 | - >>> trac.initializeRemoteBugDB([1]) |
423 | + >>> with trac.responses(): |
424 | + ... trac.initializeRemoteBugDB([1]) |
425 | >>> sorted(trac.bugs.keys()) |
426 | [1] |
427 | |
428 | If we initialize with a different set of keys we overwrite the first set: |
429 | |
430 | - >>> trac.initializeRemoteBugDB([6,7,8,9,10,11,12]) |
431 | + >>> with trac.responses(): |
432 | + ... trac.initializeRemoteBugDB([6, 7, 8, 9, 10, 11, 12]) |
433 | >>> sorted(trac.bugs.keys()) |
434 | [6, 7, 8, 9, 10, 11, 12] |
435 | |
436 | @@ -198,36 +173,41 @@ |
437 | >>> trac.batch_query_threshold |
438 | 10 |
439 | |
440 | - >>> trac.trace_calls = True |
441 | - >>> trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
442 | - CALLED urlopen(u'http://test.trac/ticket/6?format=csv') |
443 | - CALLED urlopen(u'http://test.trac/ticket/7?format=csv') |
444 | - CALLED urlopen(u'http://test.trac/ticket/8?format=csv') |
445 | - CALLED urlopen(u'http://test.trac/ticket/9?format=csv') |
446 | - CALLED urlopen(u'http://test.trac/ticket/10?format=csv') |
447 | + >>> with trac.responses(trace_calls=True): |
448 | + ... trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
449 | + GET http://test.trac/ticket/6 |
450 | + GET http://test.trac/ticket/6?format=csv |
451 | + GET http://test.trac/ticket/6?format=csv |
452 | + GET http://test.trac/ticket/7?format=csv |
453 | + GET http://test.trac/ticket/8?format=csv |
454 | + GET http://test.trac/ticket/9?format=csv |
455 | + GET http://test.trac/ticket/10?format=csv |
456 | |
457 | If there are more than batch_query_threshold bugs to update then they are |
458 | fetched as a batch: |
459 | |
460 | >>> trac.batch_query_threshold = 4 |
461 | - >>> trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
462 | - CALLED urlopen(u'http://test.trac/query?id=6&id=7...&format=csv') |
463 | + >>> with trac.responses(trace_calls=True): |
464 | + ... trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
465 | + GET http://test.trac/query?id=6&id=7...&format=csv |
466 | |
467 | The batch updating method will also be used in cases where the Trac instance |
468 | doesn't support CSV exports of individual tickets: |
469 | |
470 | >>> trac.batch_query_threshold = 10 |
471 | - >>> trac.supports_single_exports = False |
472 | - >>> trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
473 | - CALLED urlopen(u'http://test.trac/query?id=6&id=7...&format=csv') |
474 | + >>> with trac.responses(trace_calls=True, supports_single_exports=False): |
475 | + ... trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
476 | + GET http://test.trac/ticket/6 |
477 | + GET http://test.trac/ticket/6?format=csv |
478 | + GET http://test.trac/query?id=6&id=7...&format=csv |
479 | |
480 | If, when using the batch export method, the Trac instance comes across |
481 | invalid data, it will raise an UnparsableBugData exception. We will |
482 | force our trac instance to use invalid data for the purposes of this |
483 | test. |
484 | |
485 | - >>> trac.csv_export_file = 'trac_example_broken_ticket_export.csv' |
486 | - >>> trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
487 | + >>> with trac.responses(broken=True): |
488 | + ... trac.initializeRemoteBugDB([6, 7, 8, 9, 10]) |
489 | Traceback (most recent call last): |
490 | ... |
491 | UnparsableBugData: External bugtracker http://test.trac does not |
492 | @@ -236,8 +216,8 @@ |
493 | |
494 | This is also true of the single bug export mode. |
495 | |
496 | - >>> trac.supports_single_exports = True |
497 | - >>> trac.initializeRemoteBugDB([6]) |
498 | + >>> with trac.responses(broken=True): |
499 | + ... trac.initializeRemoteBugDB([6]) |
500 | Traceback (most recent call last): |
501 | ... |
502 | UnparsableBugData: External bugtracker http://test.trac does not |
503 | @@ -256,15 +236,13 @@ |
504 | method to retrieve the CSV data from the remote Trac instance. This |
505 | method accepts a URL from which to retrieve the data as a parameter. |
506 | |
507 | - >>> trac.supports_single_exports = False |
508 | - >>> trac.csv_export_file = None |
509 | - |
510 | - >>> query_url = 'http://example.com/query?id=%s&format=csv' |
511 | + >>> query_url = 'http://test.trac/query?id=%s&format=csv' |
512 | >>> query_string = '&id='.join(['1', '2', '3', '4', '5']) |
513 | >>> query_url = query_url % query_string |
514 | |
515 | - >>> remote_bugs = trac._fetchBugData(query_url) |
516 | - CALLED urlopen('http://example.com/query?id=1&id=2...&format=csv') |
517 | + >>> with trac.responses(trace_calls=True, supports_single_exports=False): |
518 | + ... remote_bugs = trac._fetchBugData(query_url) |
519 | + GET http://test.trac/query?id=1&id=2...&format=csv |
520 | |
521 | However, _fetchBugData() doesn't actually check the results it returns |
522 | except for checking that they are valid Trac CSV exports. in this case, |
523 | @@ -277,8 +255,8 @@ |
524 | If _fetchBugData() receives a response that isn't a valid Trac CSV |
525 | export, it will raise an UnparsableBugData error. |
526 | |
527 | - >>> trac.csv_export_file = 'trac_example_broken_ticket_export.csv' |
528 | - >>> trac._fetchBugData(query_url) |
529 | + >>> with trac.responses(broken=True): |
530 | + ... trac._fetchBugData(query_url) |
531 | Traceback (most recent call last): |
532 | ... |
533 | UnparsableBugData: External bugtracker http://test.trac does not |
534 | @@ -322,7 +300,9 @@ |
535 | >>> bug_watch_updater = CheckwatchesMaster( |
536 | ... txn, FakeLogger()) |
537 | >>> trac = TestTrac(example_bug_tracker.baseurl) |
538 | - >>> bug_watch_updater.updateBugWatches(trac, example_bug_tracker.watches) |
539 | + >>> with trac.responses(): |
540 | + ... bug_watch_updater.updateBugWatches( |
541 | + ... trac, example_bug_tracker.watches) |
542 | INFO Updating 1 watches for 1 bugs on http://bugs.some.where |
543 | |
544 | >>> for bug_watch in example_bug_tracker.watches: |
545 | @@ -364,10 +344,11 @@ |
546 | ... remotebug=str(remote_bug_id)) |
547 | ... bug_watches[remote_bug_id] = bug_watch.id |
548 | |
549 | - >>> trac.trace_calls = True |
550 | - >>> bug_watch_updater.updateBugWatches(trac, example_bug_tracker.watches) |
551 | + >>> with trac.responses(trace_calls=True): |
552 | + ... bug_watch_updater.updateBugWatches( |
553 | + ... trac, example_bug_tracker.watches) |
554 | INFO Updating 12 watches for 12 bugs on http://bugs.some.where |
555 | - CALLED urlopen(u'http://.../query?id=... |
556 | + GET http://bugs.some.where/query?id=... |
557 | |
558 | >>> for remote_bug_id in sorted(bug_watches.keys()): |
559 | ... bug_watch = getUtility(IBugWatchSet).get( |
560 | @@ -415,8 +396,8 @@ |
561 | u'new' |
562 | |
563 | >>> trac.batch_query_threshold = 0 |
564 | - >>> trac.trace_calls = False |
565 | - >>> bug_watch_updater.updateBugWatches(trac, [bug_watch]) |
566 | + >>> with trac.responses(): |
567 | + ... bug_watch_updater.updateBugWatches(trac, [bug_watch]) |
568 | INFO Updating 1 watches for 1 bugs on http://bugs.some.where |
569 | |
570 | >>> bug_watch.lastchanged == old_last_changed |
571 | |
572 | === modified file 'lib/lp/bugs/externalbugtracker/trac.py' |
573 | --- lib/lp/bugs/externalbugtracker/trac.py 2015-07-08 16:05:11 +0000 |
574 | +++ lib/lp/bugs/externalbugtracker/trac.py 2018-06-23 02:06:14 +0000 |
575 | @@ -6,16 +6,16 @@ |
576 | __metaclass__ = type |
577 | __all__ = ['Trac', 'TracLPPlugin'] |
578 | |
579 | -from Cookie import SimpleCookie |
580 | -from cookielib import CookieJar |
581 | import csv |
582 | from datetime import datetime |
583 | from email.utils import parseaddr |
584 | import time |
585 | -import urllib2 |
586 | import xmlrpclib |
587 | |
588 | +from mimeparse import parse_mime_type |
589 | import pytz |
590 | +import requests |
591 | +from requests.cookies import RequestsCookieJar |
592 | from zope.component import getUtility |
593 | from zope.interface import implementer |
594 | |
595 | @@ -24,13 +24,13 @@ |
596 | BugNotFound, |
597 | BugTrackerAuthenticationError, |
598 | BugTrackerConnectError, |
599 | - ExternalBugTracker, |
600 | + ExternalBugTrackerRequests, |
601 | InvalidBugId, |
602 | LookupTree, |
603 | UnknownRemoteStatusError, |
604 | UnparsableBugData, |
605 | ) |
606 | -from lp.bugs.externalbugtracker.xmlrpc import UrlLib2Transport |
607 | +from lp.bugs.externalbugtracker.xmlrpc import RequestsTransport |
608 | from lp.bugs.interfaces.bugtask import ( |
609 | BugTaskImportance, |
610 | BugTaskStatus, |
611 | @@ -44,6 +44,10 @@ |
612 | from lp.services.config import config |
613 | from lp.services.database.isolation import ensure_no_transaction |
614 | from lp.services.messages.interfaces.message import IMessageSet |
615 | +from lp.services.timeout import ( |
616 | + override_timeout, |
617 | + urlfetch, |
618 | + ) |
619 | from lp.services.webapp.url import urlappend |
620 | |
621 | # Symbolic constants used for the Trac LP plugin. |
622 | @@ -56,7 +60,7 @@ |
623 | FAULT_TICKET_NOT_FOUND = 1001 |
624 | |
625 | |
626 | -class Trac(ExternalBugTracker): |
627 | +class Trac(ExternalBugTrackerRequests): |
628 | """An ExternalBugTracker instance for handling Trac bugtrackers.""" |
629 | |
630 | ticket_url = 'ticket/%i?format=csv' |
631 | @@ -69,15 +73,16 @@ |
632 | # Any token will do. |
633 | auth_url = urlappend(base_auth_url, 'check') |
634 | try: |
635 | - response = self.urlopen(auth_url) |
636 | - except urllib2.HTTPError as error: |
637 | + with override_timeout(config.checkwatches.default_socket_timeout): |
638 | + response = urlfetch(auth_url, trust_env=False, use_proxy=True) |
639 | + except requests.HTTPError as e: |
640 | # If the error is HTTP 401 Unauthorized then we're |
641 | # probably talking to the LP plugin. |
642 | - if error.code == 401: |
643 | + if e.response.status_code == 401: |
644 | return TracLPPlugin(self.baseurl) |
645 | else: |
646 | return self |
647 | - except urllib2.URLError as error: |
648 | + except requests.RequestException: |
649 | return self |
650 | else: |
651 | # If the response contains a trac_auth cookie then we're |
652 | @@ -85,9 +90,8 @@ |
653 | # the remote system will authorize the bogus auth token we |
654 | # sent, so this check is really intended to detect broken |
655 | # Trac instances that return HTTP 200 for a missing page. |
656 | - for set_cookie in response.headers.getheaders('Set-Cookie'): |
657 | - cookie = SimpleCookie(set_cookie) |
658 | - if 'trac_auth' in cookie: |
659 | + for cookie in response.cookies: |
660 | + if cookie.name == 'trac_auth': |
661 | return TracLPPlugin(self.baseurl) |
662 | else: |
663 | return self |
664 | @@ -98,31 +102,37 @@ |
665 | |
666 | :bug_ids: A list of bug IDs that we can use for discovery purposes. |
667 | """ |
668 | - valid_ticket = False |
669 | html_ticket_url = '%s/%s' % ( |
670 | self.baseurl, self.ticket_url.replace('?format=csv', '')) |
671 | |
672 | - bug_ids = list(bug_ids) |
673 | - while not valid_ticket and len(bug_ids) > 0: |
674 | + for bug_id in bug_ids: |
675 | try: |
676 | - # We try to retrive the ticket in HTML form, since that will |
677 | - # tell us whether or not it is actually a valid ticket |
678 | - ticket_id = int(bug_ids.pop()) |
679 | - self._fetchPage(html_ticket_url % ticket_id) |
680 | - except (ValueError, urllib2.HTTPError): |
681 | - # If we get an HTTP error we can consider the ticket to be |
682 | - # invalid. If we get a ValueError then the ticket_id couldn't |
683 | - # be identified and it's of no use to us anyway. |
684 | + # We try to retrieve the ticket in HTML form, since that |
685 | + # will tell us whether or not it is actually a valid ticket. |
686 | + ticket_id = int(bug_id) |
687 | + self._getPage(html_ticket_url % ticket_id) |
688 | + except BugTrackerConnectError as e: |
689 | + if isinstance(e.error, requests.HTTPError): |
690 | + # We can consider the ticket to be invalid. |
691 | + pass |
692 | + else: |
693 | + raise |
694 | + except ValueError: |
695 | + # The ticket_id couldn't be identified and it's of no use to |
696 | + # us anyway. |
697 | pass |
698 | else: |
699 | # If we didn't get an error we can try to get the ticket in |
700 | # CSV form. If this fails then we can consider single ticket |
701 | # exports to be unsupported. |
702 | try: |
703 | - csv_data = self._fetchPage( |
704 | - "%s/%s" % (self.baseurl, self.ticket_url % ticket_id)) |
705 | - return csv_data.headers.subtype == 'csv' |
706 | - except (urllib2.HTTPError, urllib2.URLError): |
707 | + response = self._getPage( |
708 | + "%s/%s" % |
709 | + (self.baseurl, self.ticket_url % ticket_id)) |
710 | + subtype = parse_mime_type( |
711 | + response.headers.get('Content-Type', ''))[1] |
712 | + return subtype == 'csv' |
713 | + except BugTrackerConnectError: |
714 | return False |
715 | else: |
716 | # If we reach this point then we likely haven't had any valid |
717 | @@ -140,7 +150,7 @@ |
718 | """ |
719 | # We read the remote bugs into a list so that we can check that |
720 | # the data we're getting back from the remote server are valid. |
721 | - csv_reader = csv.DictReader(self._fetchPage(query_url)) |
722 | + csv_reader = csv.DictReader(self._getPage(query_url).iter_lines()) |
723 | remote_bugs = [csv_reader.next()] |
724 | |
725 | # We consider the data we're getting from the remote server to |
726 | @@ -320,9 +330,9 @@ |
727 | super(TracLPPlugin, self).__init__(baseurl) |
728 | |
729 | if cookie_jar is None: |
730 | - cookie_jar = CookieJar() |
731 | + cookie_jar = RequestsCookieJar() |
732 | if xmlrpc_transport is None: |
733 | - xmlrpc_transport = UrlLib2Transport(baseurl, cookie_jar) |
734 | + xmlrpc_transport = RequestsTransport(baseurl, cookie_jar) |
735 | |
736 | self._cookie_jar = cookie_jar |
737 | self._xmlrpc_transport = xmlrpc_transport |
738 | @@ -332,8 +342,10 @@ |
739 | self._server = xmlrpclib.ServerProxy( |
740 | xmlrpc_endpoint, transport=self._xmlrpc_transport) |
741 | |
742 | - self.url_opener = urllib2.build_opener( |
743 | - urllib2.HTTPCookieProcessor(cookie_jar)) |
744 | + def makeRequest(self, method, url, **kwargs): |
745 | + """See `ExternalBugTracker`.""" |
746 | + return super(TracLPPlugin, self).makeRequest( |
747 | + method, url, cookies=self._cookie_jar, **kwargs) |
748 | |
749 | @ensure_no_transaction |
750 | @needs_authentication |
751 | @@ -364,7 +376,7 @@ |
752 | auth_url = urlappend(base_auth_url, token_text) |
753 | |
754 | try: |
755 | - self._fetchPage(auth_url) |
756 | + self._getPage(auth_url) |
757 | except BugTrackerConnectError as e: |
758 | raise BugTrackerAuthenticationError(self.baseurl, e.error) |
759 | |
760 | |
761 | === modified file 'lib/lp/bugs/tests/externalbugtracker.py' |
762 | --- lib/lp/bugs/tests/externalbugtracker.py 2018-06-23 02:06:14 +0000 |
763 | +++ lib/lp/bugs/tests/externalbugtracker.py 2018-06-23 02:06:14 +0000 |
764 | @@ -52,10 +52,7 @@ |
765 | LP_PLUGIN_METADATA_AND_COMMENTS, |
766 | LP_PLUGIN_METADATA_ONLY, |
767 | ) |
768 | -from lp.bugs.externalbugtracker.xmlrpc import ( |
769 | - RequestsTransport, |
770 | - UrlLib2Transport, |
771 | - ) |
772 | +from lp.bugs.externalbugtracker.xmlrpc import RequestsTransport |
773 | from lp.bugs.interfaces.bugtask import ( |
774 | BugTaskImportance, |
775 | BugTaskStatus, |
776 | @@ -1199,44 +1196,45 @@ |
777 | requests_mock.add('POST', re.compile(re.escape(self.baseurl))) |
778 | |
779 | |
780 | -class TestTrac(Trac): |
781 | - """Trac ExternalBugTracker for testing purposes. |
782 | - |
783 | - It overrides urlopen, so that access to a real Trac instance isn't needed, |
784 | - and supportsSingleExports so that the tests don't fail due to the lack of |
785 | - a network connection. Also, it overrides the default batch_query_threshold |
786 | - for the sake of making test data sane. |
787 | - """ |
788 | +class TestTrac(BugTrackerResponsesMixin, Trac): |
789 | + """Trac ExternalBugTracker for testing purposes.""" |
790 | |
791 | # We remove the batch_size limit for the purposes of the tests so |
792 | # that we can test batching and not batching correctly. |
793 | batch_size = None |
794 | batch_query_threshold = 10 |
795 | csv_export_file = None |
796 | - supports_single_exports = True |
797 | - trace_calls = False |
798 | |
799 | def getExternalBugTrackerToUse(self): |
800 | return self |
801 | |
802 | - def supportsSingleExports(self, bug_ids): |
803 | - """See `Trac`.""" |
804 | - return self.supports_single_exports |
805 | - |
806 | - def urlopen(self, url, data=None): |
807 | - file_path = os.path.join(os.path.dirname(__file__), 'testfiles') |
808 | - url = url.get_full_url() |
809 | - if self.trace_calls: |
810 | - print "CALLED urlopen(%r)" % (url) |
811 | - |
812 | - if self.csv_export_file is not None: |
813 | - csv_export_file = self.csv_export_file |
814 | - elif re.match('.*/ticket/[0-9]+\?format=csv$', url): |
815 | - csv_export_file = 'trac_example_single_ticket_export.csv' |
816 | - else: |
817 | - csv_export_file = 'trac_example_ticket_export.csv' |
818 | - |
819 | - return open(file_path + '/' + csv_export_file, 'r') |
820 | + def _getCallback(self, request, supports_single_exports): |
821 | + url = urlsplit(request.url) |
822 | + if parse_qs(url.query).get('format') == ['csv']: |
823 | + mimetype = 'text/csv' |
824 | + if url.path.startswith('/ticket/'): |
825 | + if not supports_single_exports: |
826 | + return 404, {}, '' |
827 | + body = read_test_file('trac_example_single_ticket_export.csv') |
828 | + else: |
829 | + body = read_test_file('trac_example_ticket_export.csv') |
830 | + else: |
831 | + mimetype = 'text/html' |
832 | + body = '' |
833 | + return 200, {'Content-Type': mimetype}, body |
834 | + |
835 | + def addResponses(self, requests_mock, supports_single_exports=True, |
836 | + broken=False): |
837 | + url_pattern = re.compile(re.escape(self.baseurl)) |
838 | + if broken: |
839 | + requests_mock.add( |
840 | + 'GET', url_pattern, |
841 | + body=read_test_file('trac_example_broken_ticket_export.csv')) |
842 | + else: |
843 | + requests_mock.add_callback( |
844 | + 'GET', url_pattern, |
845 | + lambda request: self._getCallback( |
846 | + request, supports_single_exports)) |
847 | |
848 | |
849 | class MockTracRemoteBug: |
850 | @@ -1297,7 +1295,7 @@ |
851 | return comment |
852 | |
853 | |
854 | -class TestTracXMLRPCTransport(UrlLib2Transport): |
855 | +class TestTracXMLRPCTransport(RequestsTransport): |
856 | """An XML-RPC transport to be used when testing Trac.""" |
857 | |
858 | remote_bugs = {} |
859 | @@ -1313,8 +1311,12 @@ |
860 | |
861 | @property |
862 | def auth_cookie(self): |
863 | - cookies = self.cookie_processor.cookiejar._cookies |
864 | - return cookies.get('example.com', {}).get('', {}).get('trac_auth') |
865 | + for cookie in self.cookie_jar: |
866 | + if (cookie.domain == 'example.com' and cookie.path == '' and |
867 | + cookie.name == 'trac_auth'): |
868 | + return cookie |
869 | + else: |
870 | + return None |
871 | |
872 | @property |
873 | def has_valid_auth_cookie(self): |
874 | |
875 | === modified file 'setup.py' |
876 | --- setup.py 2018-06-06 09:01:15 +0000 |
877 | +++ setup.py 2018-06-23 02:06:14 +0000 |
878 | @@ -201,6 +201,7 @@ |
879 | 'python-debian', |
880 | 'python-keystoneclient', |
881 | 'python-memcached', |
882 | + 'python-mimeparse', |
883 | 'python-openid', |
884 | 'python-subunit', |
885 | 'python-swiftclient', |