~launchpad-pqm/launchpad/devel

14575.2.1 by Jeroen Vermeulen
Lint.
1
ExternalBugTracker: TracLPPlugin
2
================================
6037.1.6 by Bjorn Tillenius
add missing test.
3
4
This covers the implementation of the ExternalBugTracker class for Trac
5
instances having the LP XML-RPC plugin installed.
6
7
For testing purposes, a custom XML-RPC transport can be passed to it,
8
so that we can avoid network traffic in tests.
9
8523.3.10 by Gavin Panella
Fix up some externalbugtracker imports.
10
    >>> from lp.bugs.externalbugtracker import (
6037.1.6 by Bjorn Tillenius
add missing test.
11
    ...     TracLPPlugin)
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
12
    >>> from lp.bugs.tests.externalbugtracker import (
6037.1.6 by Bjorn Tillenius
add missing test.
13
    ...     TestTracXMLRPCTransport)
6604.1.12 by Tom Berger
post review and fixes
14
    >>> test_transport = TestTracXMLRPCTransport('http://example.com/')
6037.1.6 by Bjorn Tillenius
add missing test.
15
    >>> trac = TracLPPlugin(
16
    ...     'http://example.com/', xmlrpc_transport=test_transport)
6972.6.1 by Graham Binns
Added _s to the xmlrpc_transport and internal_* attributes of TracLPPlugin. Moved the xmlrpc server proxy that got created in all the methods into an attribute.
17
    >>> trac._xmlrpc_transport is test_transport
6002.5.5 by Bjorn Tillenius
make sure the Cookie header gets set
18
    True
6037.1.6 by Bjorn Tillenius
add missing test.
19
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
20
14575.2.1 by Jeroen Vermeulen
Lint.
21
Authentication
22
--------------
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
23
24
Before any XML-RPC methods can be used, we need to authenticate with the
6002.5.16 by Bjorn Tillenius
improve doctest.
25
Trac instance. To authenticate we create a special login token in
26
Launchpad. We then give that token to the Trac instance, which checks
27
whether the token is valid, and returns a cookie we can send with the
28
XML-RPC requests.
29
30
We give the token to Trac by issuing a HTTP request at
31
$base_url/launchpad-auth/$token. A request to such an URL will cause
32
Trac to validate $token and return a Set-Cookie header.
6002.5.10 by Bjorn Tillenius
use XML-RPC to generate the token.
33
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
34
    >>> import random
14557.1.9 by Curtis Hovey
Moved logintoken to lp.verification.
35
    >>> from lp.services.verification.interfaces.logintoken import (
36
    ...     ILoginTokenSet)
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
37
    >>> from lp.services.webapp.url import urlappend
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
38
    >>> from lp.testing.dbuser import lp_dbuser
6002.5.16 by Bjorn Tillenius
improve doctest.
39
40
    >>> class FakeResponse:
41
    ...     def __init__(self):
42
    ...         self.headers = {}
43
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
44
    >>> class TestTracLPPlugin(TracLPPlugin):
12476.2.3 by Tim Penhey
Make all the testing external bugtrackers honour the function signature.
45
    ...     def urlopen(self, url, data=None):
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
46
    ...         with lp_dbuser():
47
    ...             base_auth_url = urlappend(self.baseurl, 'launchpad-auth')
48
    ...             if not url.startswith(base_auth_url + '/'):
49
    ...                 raise AssertionError("Unexpected URL: %s" % url)
50
    ...             token_text = url.split('/')[-1]
51
    ...             token = getUtility(ILoginTokenSet)[token_text]
52
    ...             if token.tokentype.name != 'BUGTRACKER':
53
    ...                 raise AssertionError(
54
    ...                     'Invalid token type: %s' % token.tokentype.name)
55
    ...             if token.date_consumed is not None:
56
    ...                 raise AssertionError(
57
    ...                     "Token has already been consumed.")
58
    ...             token.consume()
59
    ...             print "Successfully validated the token."
60
    ...             cookie_string = (
61
    ...                 'trac_auth=random_token-' + str(random.random()))
62
    ...             self._xmlrpc_transport.setCookie(cookie_string)
63
    ...             response = FakeResponse()
64
    ...             response.headers['Set-Cookie'] = cookie_string
7130.2.2 by Graham Binns
TracLPPlugin now shares a CookieJar between its url opener (used for authentication) and its XML-RPC transport.
65
    ...
6002.5.4 by Bjorn Tillenius
make sure that the auth_cookie gets set on the transport.
66
    ...         return response
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
67
6002.5.16 by Bjorn Tillenius
improve doctest.
68
To generate the token, the internal XML-RPC server is used. By using the
69
XML-RPC server rather than talking to the database directy means that we
70
don't have to bother about commiting the transaction to make the token
71
visible to Trac.
72
7130.2.2 by Graham Binns
TracLPPlugin now shares a CookieJar between its url opener (used for authentication) and its XML-RPC transport.
73
    >>> from cookielib import CookieJar
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
74
    >>> from lp.bugs.tests.externalbugtracker import (
6532.1.3 by Graham Binns
Added tests for _authenticate.
75
    ...     TestInternalXMLRPCTransport)
7130.2.2 by Graham Binns
TracLPPlugin now shares a CookieJar between its url opener (used for authentication) and its XML-RPC transport.
76
    >>> cookie_jar = CookieJar()
77
    >>> test_transport = TestTracXMLRPCTransport(
78
    ...     'http://example.com/', cookie_jar)
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
79
    >>> trac = TestTracLPPlugin(
6002.5.10 by Bjorn Tillenius
use XML-RPC to generate the token.
80
    ...     'http://example.com/', xmlrpc_transport=test_transport,
7130.2.2 by Graham Binns
TracLPPlugin now shares a CookieJar between its url opener (used for authentication) and its XML-RPC transport.
81
    ...     internal_xmlrpc_transport=TestInternalXMLRPCTransport(),
82
    ...     cookie_jar=cookie_jar)
6002.5.16 by Bjorn Tillenius
improve doctest.
83
84
The method that authenticates with Trac is _authenticate().
85
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
86
    >>> trac._authenticate()
6002.5.18 by Bjorn Tillenius
review fixes.
87
    Using XML-RPC to generate token.
6002.5.3 by Bjorn Tillenius
make sure a valid authentication token is generated.
88
    Successfully validated the token.
89
6002.5.16 by Bjorn Tillenius
improve doctest.
90
After it has been called, the XML-RPC transport will have its
91
auth_cookie attribute set.
92
6604.1.12 by Tom Berger
post review and fixes
93
    >>> test_transport.cookie_processor.cookiejar
94
    <cookielib.CookieJar[Cookie(version=0, name='trac_auth'...
6002.5.4 by Bjorn Tillenius
make sure that the auth_cookie gets set on the transport.
95
7130.2.2 by Graham Binns
TracLPPlugin now shares a CookieJar between its url opener (used for authentication) and its XML-RPC transport.
96
The XML-RPC transport's cookie_processor shares its cookiejar with the
97
TracLPPlugin instance. This is so that the TracLPPlugin can use the
98
cookiejar when authenticating with the remote Trac and then pass it to
99
the XML-RPC transport for further use, meaning that there's no need to
100
manually manipulate cookies.
101
102
    >>> test_transport.cookie_processor.cookiejar == trac._cookie_jar
103
    True
6002.5.5 by Bjorn Tillenius
make sure the Cookie header gets set
104
7130.2.5 by Graham Binns
Added some tests to cover the sharing of the CookieJar.
105
So if we look in the TracLPPlugin's CookieJar we'll see the same cookie:
106
107
    >>> trac._cookie_jar
108
    <cookielib.CookieJar[Cookie(version=0, name='trac_auth'...
109
110
And altering the cookie in the TracLPPlugin's CookieJar will mean, of
111
course, that it's altered in the XML-RPC transport's CookieJar, too.
112
113
    >>> from cookielib import Cookie
114
    >>> new_cookie = Cookie(
115
    ...     name="trac_auth",
116
    ...     value="Look ma, a new cookie!",
117
    ...     version=0, port=None, port_specified=False,
118
    ...     domain='http://example.com', domain_specified=True,
119
    ...     domain_initial_dot=None, path='', path_specified=False,
120
    ...     secure=False, expires=False, discard=None, comment=None,
121
    ...     comment_url=None, rest=None)
122
123
    >>> trac._cookie_jar.clear()
124
    >>> trac._cookie_jar.set_cookie(new_cookie)
125
126
    >>> trac._cookie_jar
127
    <cookielib.CookieJar[Cookie(version=0, name='trac_auth',
128
    value='Look ma, a new cookie!'...>
129
130
    >>> test_transport.cookie_processor.cookiejar
131
    <cookielib.CookieJar[Cookie(version=0, name='trac_auth',
132
    value='Look ma, a new cookie!'...>
133
7130.2.6 by Graham Binns
A BugTrackerAuthenticationError will now be raised if we can't auth with the Trac instance.
134
If authentication fails, a BugTrackerAuthenticationError will be raised.
135
136
    >>> from urllib2 import HTTPError
137
    >>> class TestFailingTracLPPlugin(TracLPPlugin):
12476.2.3 by Tim Penhey
Make all the testing external bugtrackers honour the function signature.
138
    ...     def urlopen(self, url, data=None):
7130.2.6 by Graham Binns
A BugTrackerAuthenticationError will now be raised if we can't auth with the Trac instance.
139
    ...         raise HTTPError(url, 401, "Denied!", {}, None)
140
141
    >>> test_trac = TestFailingTracLPPlugin(
142
    ...     'http://example.com', xmlrpc_transport=test_transport,
143
    ...     internal_xmlrpc_transport=TestInternalXMLRPCTransport(),
144
    ...     cookie_jar=cookie_jar)
145
    >>> test_trac._authenticate()
146
    Traceback (most recent call last):
147
      ...
7130.2.7 by Graham Binns
TracLPPlugin now passes its baseurl to BugTrackerAuthenticationError. This should hopefully serve to consolidate some OOPSes.
148
    BugTrackerAuthenticationError: http://example.com:
12558.1.2 by William Grant
Fix Trac to use _fetchPage (which catches URLError and HTTPError) where possible, and catch both where it's not possible. This mostly affects the initial contact with each tracker, getExternalBugTrackerToUse.
149
    HTTP Error 401: Denied!
7130.2.6 by Graham Binns
A BugTrackerAuthenticationError will now be raised if we can't auth with the Trac instance.
150
6002.5.7 by Bjorn Tillenius
make getCurrentDBTime() authenticate.
151
14575.2.1 by Jeroen Vermeulen
Lint.
152
Current time
153
------------
6037.1.6 by Bjorn Tillenius
add missing test.
154
155
The current time is always returned in UTC, no matter if the Trac
156
instance returns another time zone.
157
6604.1.12 by Tom Berger
post review and fixes
158
    >>> test_transport = TestTracXMLRPCTransport('http://example.com/')
6002.5.5 by Bjorn Tillenius
make sure the Cookie header gets set
159
    >>> trac = TestTracLPPlugin(
6002.5.10 by Bjorn Tillenius
use XML-RPC to generate the token.
160
    ...     'http://example.com/', xmlrpc_transport=test_transport,
6532.1.3 by Graham Binns
Added tests for _authenticate.
161
    ...     internal_xmlrpc_transport=TestInternalXMLRPCTransport())
6002.5.5 by Bjorn Tillenius
make sure the Cookie header gets set
162
6037.1.6 by Bjorn Tillenius
add missing test.
163
    >>> from datetime import datetime
6275.1.2 by Bjorn Tillenius
add comment.
164
    >>> # There doesn't seem to be a way to generate a UTC time stamp,
165
    >>> # without mocking around with the TZ environment variable.
6275.1.1 by Bjorn Tillenius
use utcfromtimestamp instead of fromtimestamp.
166
    >>> datetime.utcfromtimestamp(1207706521)
167
    datetime.datetime(2008, 4, 9, 2, 2, 1)
168
169
    >>> HOUR = 60*60
170
    >>> test_transport.seconds_since_epoch = 1207706521 + HOUR
6037.1.6 by Bjorn Tillenius
add missing test.
171
    >>> test_transport.local_timezone = 'CET'
6275.1.1 by Bjorn Tillenius
use utcfromtimestamp instead of fromtimestamp.
172
    >>> test_transport.utc_offset = HOUR
6037.1.6 by Bjorn Tillenius
add missing test.
173
    >>> trac.getCurrentDBTime()
6002.5.18 by Bjorn Tillenius
review fixes.
174
    Using XML-RPC to generate token.
6002.5.7 by Bjorn Tillenius
make getCurrentDBTime() authenticate.
175
    Successfully validated the token.
6037.1.6 by Bjorn Tillenius
add missing test.
176
    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
177
178
An authorization request was automatically sent, since the method needed
179
authentication. Because the cookie is now set, other calls won't cause
6002.5.18 by Bjorn Tillenius
review fixes.
180
an authorization request.
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
181
182
    >>> test_transport.auth_cookie
6604.1.12 by Tom Berger
post review and fixes
183
    Cookie(version=0, name='trac_auth'...)
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
184
    >>> trac.getCurrentDBTime()
185
    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
186
187
If the cookie gets expired, an authorization request is automatically
188
sent again.
189
6002.5.15 by Bjorn Tillenius
some cleanup
190
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
191
    >>> trac.getCurrentDBTime()
6002.5.18 by Bjorn Tillenius
review fixes.
192
    Using XML-RPC to generate token.
6002.5.8 by Bjorn Tillenius
add needs_authentication decorator.
193
    Successfully validated the token.
194
    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
6037.3.3 by Graham Binns
Began implementation.
195
196
14575.2.1 by Jeroen Vermeulen
Lint.
197
Getting modified bugs
198
---------------------
6037.3.3 by Graham Binns
Began implementation.
199
200
We only want to update the bug watches whose remote bugs have been
201
modified since the last time we checked.
202
203
In order to demonstrate this, we'll create some mock remote bugs for our
204
test XML-RPC transport to check.
205
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
206
    >>> from lp.bugs.tests.externalbugtracker import (
6037.3.3 by Graham Binns
Began implementation.
207
    ...     MockTracRemoteBug)
208
209
    >>> remote_bugs = {
210
    ...     '1': MockTracRemoteBug('1', datetime(2008, 4, 1, 0, 0, 0)),
211
    ...     '2': MockTracRemoteBug('2', datetime(2007, 1, 1, 1, 1, 1)),
6037.3.16 by Graham Binns
review changes.
212
    ...     '3': MockTracRemoteBug('3', datetime(2008, 1, 1, 1, 2, 3)),
213
    ...     }
6037.3.3 by Graham Binns
Began implementation.
214
215
    >>> test_transport.remote_bugs = remote_bugs
6037.3.5 by Graham Binns
Added implementation of getModifiedRemoteBugs().
216
6037.3.17 by Graham Binns
review changes.
217
Calling the getModifiedRemoteBugs() method of our Trac instance and
6037.3.14 by Graham Binns
Review changes.
218
passing it a list of bug IDs and a datetime object will return a list
219
of the IDs of the bugs which have been modified since that time.
6037.3.5 by Graham Binns
Added implementation of getModifiedRemoteBugs().
220
221
    >>> bug_ids_to_check = ['1', '2', '3']
6037.3.6 by Graham Binns
Added implementation of getModifiedRemoteBugs().
222
    >>> last_checked = datetime(2008, 1, 1, 0, 0, 0)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
223
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6037.3.17 by Graham Binns
review changes.
224
    >>> sorted(trac.getModifiedRemoteBugs(
225
    ...     bug_ids_to_check, last_checked))
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
226
    Using XML-RPC to generate token.
227
    Successfully validated the token.
6037.3.5 by Graham Binns
Added implementation of getModifiedRemoteBugs().
228
    ['1', '3']
229
6037.3.9 by Graham Binns
Added a fix for the really weird XML-RPC behaviour with list-length-1 problems.
230
Different last_checked times will result in different numbers of bugs
231
being returned.
232
233
    >>> last_checked = datetime(2008, 2, 1, 0, 0, 0)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
234
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6037.3.9 by Graham Binns
Added a fix for the really weird XML-RPC behaviour with list-length-1 problems.
235
    >>> trac.getModifiedRemoteBugs(
236
    ...     bug_ids_to_check, last_checked)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
237
    Using XML-RPC to generate token.
238
    Successfully validated the token.
6037.3.9 by Graham Binns
Added a fix for the really weird XML-RPC behaviour with list-length-1 problems.
239
    ['1']
240
6037.3.6 by Graham Binns
Added implementation of getModifiedRemoteBugs().
241
If no bugs have been updated since last_checked, getModifiedRemoteBugs()
242
will return an empty list.
243
6037.3.8 by Graham Binns
Attempted (and failed) to fix a weird bug whereby the TestTracXMLRPCTransport will explode lists of length 1 when returning them.w
244
    >>> last_checked = datetime(2008, 5, 1, 0, 0, 0)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
245
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6037.3.6 by Graham Binns
Added implementation of getModifiedRemoteBugs().
246
    >>> trac.getModifiedRemoteBugs(
247
    ...     bug_ids_to_check, last_checked)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
248
    Using XML-RPC to generate token.
249
    Successfully validated the token.
6037.3.6 by Graham Binns
Added implementation of getModifiedRemoteBugs().
250
    []
6037.3.7 by Graham Binns
Attempted (and failed) to fix a weird bug whereby the TestTracXMLRPCTransport will explode lists of length 1 when returning them.w
251
6037.3.23 by Graham Binns
Removed the logic from getModifiedRemoteBugs() that dropped missing remote bugs.
252
If we ask for bug ids that don't exist on the remote server, they will
253
also be returned. This is so that when we try to retrieve the status of
254
the missing bugs an error will be raised that we can then investigate.
6037.3.21 by Graham Binns
getModifiedRemoteBugs() no longer returns non-existant bugs.
255
256
    >>> bug_ids_to_check = ['1', '2', '3', '99', '100']
257
    >>> last_checked = datetime(2008, 1, 1, 0, 0, 0)
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
258
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6037.3.21 by Graham Binns
getModifiedRemoteBugs() no longer returns non-existant bugs.
259
    >>> sorted(trac.getModifiedRemoteBugs(
260
    ...     bug_ids_to_check, last_checked))
6037.3.25 by Graham Binns
Added @needs_authentication decorator.
261
    Using XML-RPC to generate token.
262
    Successfully validated the token.
6037.3.23 by Graham Binns
Removed the logic from getModifiedRemoteBugs() that dropped missing remote bugs.
263
    ['1', '100', '3', '99']
6037.3.21 by Graham Binns
getModifiedRemoteBugs() no longer returns non-existant bugs.
264
6037.5.1 by Graham Binns
Began adding tests.
265
14575.2.1 by Jeroen Vermeulen
Lint.
266
Getting the status of remote bugs
267
---------------------------------
6037.5.1 by Graham Binns
Began adding tests.
268
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
269
Like all other ExternalBugTrackers, the TracLPPlugin ExternalBugTracker
6037.5.1 by Graham Binns
Began adding tests.
270
allows us to fetch bugs statuses from the remote bug tracker.
271
6037.5.3 by Graham Binns
Added tests to cover bug 203564.
272
To demonstrate this, we'll add some statuses to our mock remote bugs.
273
274
    >>> test_transport.remote_bugs['1'].status = 'open'
275
    >>> test_transport.remote_bugs['2'].status = 'fixed'
276
    >>> test_transport.remote_bugs['3'].status = 'reopened'
277
278
We need to call initializeRemoteBugDB() on our TracLPPlugin instance to
279
be able to retrieve remote statuses.
280
6037.5.4 by Graham Binns
Made TracLPPlugin extend Trac.
281
    >>> last_checked = datetime(2008, 1, 1, 0, 0, 0)
6037.5.3 by Graham Binns
Added tests to cover bug 203564.
282
    >>> bugs_to_update = trac.getModifiedRemoteBugs(
283
    ...     bug_ids_to_check, last_checked)
6037.5.22 by Graham Binns
Updated tests to include @needs_authentication changes.
284
    >>> test_transport.expireCookie(test_transport.auth_cookie)
6037.5.3 by Graham Binns
Added tests to cover bug 203564.
285
    >>> trac.initializeRemoteBugDB(bugs_to_update)
6037.5.22 by Graham Binns
Updated tests to include @needs_authentication changes.
286
    Using XML-RPC to generate token.
287
    Successfully validated the token.
6037.5.3 by Graham Binns
Added tests to cover bug 203564.
288
289
Calling getRemoteStatus() on our example TracLPPlugin instance will
290
return the status for whichever bug we request.
291
292
    >>> trac.getRemoteStatus('1')
293
    'open'
294
6037.5.8 by Graham Binns
Added an implementation of TracLPPlugin.initializeRemoteBugDB().
295
    >>> trac.getRemoteStatus('3')
296
    'reopened'
297
298
If we try to get the status of bug 2 we'll get a BugNotFound error,
299
since that bug wasn't in the list of bugs that were modified since our
300
last_checked time.
301
302
    >>> trac.getRemoteStatus('2')
6037.5.9 by Graham Binns
Fixed a really weird test failure.
303
    Traceback (most recent call last):
6037.5.8 by Graham Binns
Added an implementation of TracLPPlugin.initializeRemoteBugDB().
304
      ...
305
    BugNotFound: 2
306
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
307
14575.2.1 by Jeroen Vermeulen
Lint.
308
Importing Comments
309
------------------
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
310
311
The TracLPPlugin class allows Launchpad to import comments from remote
312
systems that have the Launchpad plugin installed.
313
314
TracLPPlugin implements the ISupportsCommentImport interface, providing
315
three methods: getCommentIds(), getPosterForComment() and
316
getMessageForComment().
317
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
318
    >>> from lp.bugs.interfaces.externalbugtracker import (
319
    ...     ISupportsCommentImport)
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
320
    >>> ISupportsCommentImport.providedBy(trac)
321
    True
322
323
We'll add some comments to our example bugs in order to demonstrate the
324
comment importing functionality.
325
6037.9.10 by Graham Binns
Added tests and implementation for getMessagesForComment().
326
    >>> import time
327
    >>> comment_datetime = datetime(2008, 4, 18, 17, 0, 0)
328
    >>> comment_timestamp = int(time.mktime(comment_datetime.timetuple()))
329
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
330
    >>> test_transport.remote_bugs['1'].comments = [
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
331
    ...     {'id': '1-1', 'type': 'comment',
332
    ...      'user': 'Test <test@canonical.com>',
6037.9.10 by Graham Binns
Added tests and implementation for getMessagesForComment().
333
    ...      'comment': 'Hello, world!',
6037.9.22 by Graham Binns
Altered Trac LP plugin mockup to always return 'timestamp' for timestamps.
334
    ...      'timestamp': comment_timestamp}]
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
335
    >>> test_transport.remote_bugs['2'].comments = [
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
336
    ...     {'id': '2-1', 'type': 'comment', 'user': 'test@canonical.com',
6037.9.10 by Graham Binns
Added tests and implementation for getMessagesForComment().
337
    ...      'comment': 'Hello again, world!',
6037.9.22 by Graham Binns
Altered Trac LP plugin mockup to always return 'timestamp' for timestamps.
338
    ...      'timestamp': comment_timestamp},
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
339
    ...     {'id': '2-2', 'type': 'comment', 'user': 'foo.bar',
6037.9.10 by Graham Binns
Added tests and implementation for getMessagesForComment().
340
    ...      'comment': 'More commentary.',
6037.9.22 by Graham Binns
Altered Trac LP plugin mockup to always return 'timestamp' for timestamps.
341
    ...      'timestamp': comment_timestamp}]
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
342
343
We also need an example Bug, BugTracker and BugWatch.
344
11716.1.12 by Curtis Hovey
Sorted imports in doctests.
345
    >>> from lp.bugs.interfaces.bug import CreateBugParams
11716.1.6 by Curtis Hovey
Converted glob imports in doctests to import for the true module.
346
    >>> from lp.bugs.interfaces.bugtracker import BugTrackerType
347
    >>> from lp.registry.interfaces.person import IPersonSet
348
    >>> from lp.registry.interfaces.product import IProductSet
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
349
    >>> from lp.bugs.tests.externalbugtracker import (
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
350
    ...     new_bugtracker)
351
352
    >>> bug_tracker = new_bugtracker(BugTrackerType.TRAC)
353
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
354
    >>> with lp_dbuser():
355
    ...     sample_person = getUtility(IPersonSet).getByEmail(
356
    ...         'test@canonical.com')
357
    ...     firefox = getUtility(IProductSet).getByName('firefox')
358
    ...     bug = firefox.createBug(
359
    ...         CreateBugParams(sample_person, "Yet another test bug",
360
    ...             "Yet another test description.",
361
    ...             subscribe_owner=False))
362
    ...     bug_watch = bug.addWatch(bug_tracker, '1', sample_person)
363
    ...     bug_watch_two = bug.addWatch(bug_tracker, '2', sample_person)
364
    ...     bug_watch_three = bug.addWatch(bug_tracker, '3', sample_person)
365
    ...     bug_watch_broken = bug.addWatch(bug_tracker, '123', sample_person)
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
366
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
367
getCommentIds() returns all the comment IDs for a given remote bug.
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
368
bug_watch is against remote bug 1, which has one comment.
369
370
    >>> test_transport.expireCookie(test_transport.auth_cookie)
371
    >>> bugs_to_update = ['1', '2', '3']
372
    >>> trac.initializeRemoteBugDB(bugs_to_update)
373
    Using XML-RPC to generate token.
374
    Successfully validated the token.
375
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
376
    >>> trac.getCommentIds(bug_watch.remotebug)
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
377
    ['1-1']
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
378
379
bug_watch_two is against remote bug 2, which has two comments.
380
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
381
    >>> trac.getCommentIds(bug_watch_two.remotebug)
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
382
    ['2-1', '2-2']
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
383
384
bug_watch_three is against bug 3, which has no comments.
385
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
386
    >>> trac.getCommentIds(bug_watch_three.remotebug)
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
387
    []
388
389
Trying to call getCommentIds() on a bug that doesn't exist will raise a
390
BugNotFound error.
391
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
392
    >>> trac.getCommentIds(bug_watch_broken.remotebug)
6037.9.2 by Graham Binns
Added tests and implementation for getCommentIds().
393
    Traceback (most recent call last):
394
      ...
395
    BugNotFound: 123
396
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
397
The fetchComments() method is used to pre-load a given set of comments
398
for a given bug before they are parsed.
399
400
Before fetchComments() is called for a given remote bug, that remote
401
bug's 'comments' field will be a list of comment IDs.
402
403
    >>> trac.bugs[1]['comments']
404
    ['1-1']
405
406
After fetchComments() is called the bug's 'comments' field will contain
407
a dict in the form {<comment_id>: <comment_dict>}, which can then be
408
parsed.
409
10512.4.24 by Gavin Panella
Get all the externalbugtracker*.txt doctests passing.
410
    >>> remote_bug = bug_watch.remotebug
411
    >>> transaction.commit()
412
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
413
    >>> test_transport.expireCookie(test_transport.auth_cookie)
10512.4.24 by Gavin Panella
Get all the externalbugtracker*.txt doctests passing.
414
    >>> trac.fetchComments(remote_bug, ['1-1'])
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
415
    Using XML-RPC to generate token.
416
    Successfully validated the token.
417
6037.9.22 by Graham Binns
Altered Trac LP plugin mockup to always return 'timestamp' for timestamps.
418
    >>> for comment in trac.bugs[1]['comments'].values():
419
    ...     for key in sorted(comment.keys()):
420
    ...         print "%s: %s" % (key, comment[key])
421
    comment: Hello, world!
422
    id: 1-1
423
    timestamp: 1208518200
424
    type: comment
425
    user: Test <test@canonical.com>
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
426
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
427
getPosterForComment() returns a tuple of (displayname, emailaddress) for
428
the poster of a given comment.
429
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
430
    >>> trac.getPosterForComment(bug_watch.remotebug, '1-1')
6037.9.5 by Graham Binns
getCommentIds() now works correctly.
431
    ('Test', 'test@canonical.com')
432
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
433
getPosterForComment() handles situations in which only an email address
434
is supplied for the 'user' field by returning None as the user's
6037.9.15 by Graham Binns
Review fixes.
435
displayname. When this is passed to IPersonSet.ensurePerson() a display
436
name will be generated for the user from their email address.
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
437
10512.4.24 by Gavin Panella
Get all the externalbugtracker*.txt doctests passing.
438
    >>> remote_bug = bug_watch_two.remotebug
439
    >>> transaction.commit()
440
441
    >>> trac.fetchComments(remote_bug, ['2-1', '2-2'])
442
    >>> trac.getPosterForComment(remote_bug, '2-1')
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
443
    (None, 'test@canonical.com')
444
445
getPosterForComment() will also return displayname, email tuples in
446
cases where the 'user' field is set to a plain username (e.g. 'foo').
6325.2.6 by Graham Binns
Added implementation for Trac.
447
However, in these cases it is the email address that will be set to
448
None.
6037.9.7 by Graham Binns
Added tests and implementation for getPosterForComment().
449
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
450
    >>> trac.getPosterForComment(bug_watch_two.remotebug, '2-2')
6325.2.6 by Graham Binns
Added implementation for Trac.
451
    ('foo.bar', None)
6037.9.8 by Graham Binns
Added tests and implementation of getMessageForComment().
452
453
Finally, getMessageForComment() will return a Message instance for a
454
given comment. For the sake of brevity we'll use test@canonical.com as
455
the comment's poster.
456
457
    >>> from zope.component import getUtility
458
    >>> poster = getUtility(IPersonSet).getByEmail('test@canonical.com')
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
459
    >>> message_one = trac.getMessageForComment(
460
    ...     bug_watch.remotebug, '1-1', poster)
6037.9.8 by Graham Binns
Added tests and implementation of getMessageForComment().
461
6037.9.10 by Graham Binns
Added tests and implementation for getMessagesForComment().
462
The Message returned by getMessageForComment() contains the full text of
463
the original comment.
464
465
    >>> print message_one.text_contents
466
    Hello, world!
467
7182.1.1 by Graham Binns
Fixed bug 283275.
468
The owner of the comment is set to the Person passed to
469
getMessageForComment().
470
471
    >>> print message_one.owner.displayname
472
    Sample Person
473
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
474
14575.2.1 by Jeroen Vermeulen
Lint.
475
Pushing comments
476
----------------
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
477
478
The TracLPPlugin ExternalBugTracker implements the
479
ISupportsCommentPushing interface, which allows Launchpad to use it to
480
push comments to the remote bug tracker.
481
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
482
    >>> from lp.bugs.interfaces.externalbugtracker import (
483
    ...     ISupportsCommentPushing)
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
484
    >>> ISupportsCommentPushing.providedBy(trac)
485
    True
486
487
ISupportsCommentPushing defines a method, addRemoteComment(), which is
488
responsible for pushing comments to the remote bug tracker. It accepts
489
two parameters: the ID of the remote bug to which to push the comment
490
and a Message instance containing the comment to be pushed. It returns
491
the ID assigned to the comment by the remote bug tracker.
492
493
To demonstrate this method, we'll create a comment to push.
494
12929.9.7 by j.c.sackett
Caught more imports.
495
    >>> from lp.services.messages.interfaces.message import IMessageSet
7675.1043.3 by Gary Poster
add tests for dbuser, tweak implementation slightly, and make a few more tests use it.
496
    >>> with lp_dbuser():
497
    ...     message = getUtility(IMessageSet).fromText(
498
    ...         "A subject", "An example comment to push.", poster)
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
499
500
Calling addRemoteComment() on our TracLPPlugin instance will push the
501
comment to the remote bug tracker. We'll add it to bug three on the
502
remote tracker, which as yet has no comments.
503
504
    >>> test_transport.remote_bugs['3'].comments
505
    []
506
507
addRemoteComment() requires authentication with the remote trac
508
instance. We'll expire our auth cookie to demonstrate this.
509
510
    >>> test_transport.expireCookie(test_transport.auth_cookie)
511
10512.4.24 by Gavin Panella
Get all the externalbugtracker*.txt doctests passing.
512
    >>> message_text_contents = message.text_contents
513
    >>> message_rfc822msgid = message.rfc822msgid
514
    >>> transaction.commit()
515
6253.1.3 by Graham Binns
Updated TracLPPlugin.addRemoteComment() and associated tests.
516
    >>> remote_comment_id = trac.addRemoteComment(
10512.4.24 by Gavin Panella
Get all the externalbugtracker*.txt doctests passing.
517
    ...     '3', message_text_contents, message_rfc822msgid)
6037.10.2 by Graham Binns
Manually recreated the push-lpplugin-comments patch.
518
    Using XML-RPC to generate token.
519
    Successfully validated the token.
520
521
    >>> print remote_comment_id
522
    3-1
523
524
If we look at our example remote server we can see that the comment has
525
been pushed to bug 3.
526
527
    >>> for comment in test_transport.remote_bugs['3'].comments:
528
    ...     for key in sorted(comment.keys()):
529
    ...         print "%s: %s" % (key, comment[key])
530
    comment: An example comment to push.
531
    id: 3-1
532
    time: ...
533
    type: comment
534
    user: launchpad
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
535
536
14575.2.1 by Jeroen Vermeulen
Lint.
537
Linking remote bugs to Launchpad bugs
538
-------------------------------------
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
539
540
The TracLPPlugin class implements the ISupportsBackLinking interface,
541
which allows it to tell the remote bug tracker which Launchpad bug
542
links to a given one of its bugs.
543
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
544
    >>> from lp.bugs.interfaces.externalbugtracker import (
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
545
    ...     ISupportsBackLinking)
546
    >>> from zope.interface.verify import verifyObject
547
    >>> verifyObject(ISupportsBackLinking, trac)
6972.6.6 by Graham Binns
Added implementation for ISupportsBackLinking in TracLPPlugin.
548
    True
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
549
550
The getLaunchpadBugId() method will return the Launchpad bug ID for a
551
given remote bug. If no Launchpad bug has been linked to the remote bug,
552
getLaunchpadBugId() will return None.
553
554
getLaunchpadBugId() requires authentication.
555
556
    >>> test_transport.expireCookie(test_transport.auth_cookie)
557
    >>> launchpad_bug_id = trac.getLaunchpadBugId('3')
558
    Using XML-RPC to generate token.
559
    Successfully validated the token.
560
561
    >>> print launchpad_bug_id
562
    None
563
564
We call setLaunchpadBugId() to set the Launchpad bug ID for a remote
565
bug. setLaunchpadBugId() also requires authentication.
566
567
    >>> test_transport.expireCookie(test_transport.auth_cookie)
10694.2.22 by Gavin Panella
Pass the canonical URL of the Launchpad bug into setLaunchpadBugId().
568
    >>> trac.setLaunchpadBugId('3', 15, 'http://bugs.launchpad.dev/bugs/xxx')
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
569
    Using XML-RPC to generate token.
570
    Successfully validated the token.
571
572
Calling getLaunchpadBugId() for remote bug 3 will now return 10, since
573
that's the Launchpad bug ID that we've just set.
574
575
    >>> print trac.getLaunchpadBugId('3')
6972.6.6 by Graham Binns
Added implementation for ISupportsBackLinking in TracLPPlugin.
576
    15
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
577
578
Passing a Launchpad bug ID of None to setLaunchpadBugId() will unset the
579
Launchpad bug ID for the remote bug.
580
10694.2.22 by Gavin Panella
Pass the canonical URL of the Launchpad bug into setLaunchpadBugId().
581
    >>> trac.setLaunchpadBugId('3', None, None)
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
582
    >>> print trac.getLaunchpadBugId('3')
583
    None
584
585
If we try to call getLaunchpadBugId() or setLaunchpadBugId() for a
586
remote bug that doesn't exist, a BugNotFound error will be raised.
587
588
    >>> trac.getLaunchpadBugId('12345')
589
    Traceback (most recent call last):
590
      ...
591
    BugNotFound: 12345
592
10694.2.22 by Gavin Panella
Pass the canonical URL of the Launchpad bug into setLaunchpadBugId().
593
    >>> trac.setLaunchpadBugId(
594
    ...     '12345', 1, 'http://bugs.launchpad.dev/bugs/xxx')
6972.6.5 by Graham Binns
Added tests for TracLPPlugin implementation of ISupportsBackLinking.
595
    Traceback (most recent call last):
596
      ...
597
    BugNotFound: 12345