~launchpad-pqm/launchpad/devel

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