blob: b8b638ed28e5e9004c2cba9ecfd0436c840f5fa6 [file] [log] [blame]
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Http tests
18
19Unit tests for the apiclient.http.
20"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
23
Joe Gregorio7cbceab2011-06-27 10:46:54 -040024# Do not remove the httplib2 import
25import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import os
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050027import unittest
Joe Gregorio910b9b12012-06-12 09:36:30 -040028import StringIO
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050029
Joe Gregorio66f57522011-11-30 11:00:00 -050030from apiclient.errors import BatchError
31from apiclient.http import BatchHttpRequest
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050032from apiclient.http import HttpMockSequence
Joe Gregoriod0bd3882011-11-22 09:49:47 -050033from apiclient.http import HttpRequest
Joe Gregoriod0bd3882011-11-22 09:49:47 -050034from apiclient.http import MediaFileUpload
Joe Gregorio66f57522011-11-30 11:00:00 -050035from apiclient.http import MediaUpload
Ali Afshar6f11ea12012-02-07 10:32:14 -050036from apiclient.http import MediaInMemoryUpload
Joe Gregorio910b9b12012-06-12 09:36:30 -040037from apiclient.http import MediaIoBaseUpload
Joe Gregorio66f57522011-11-30 11:00:00 -050038from apiclient.http import set_user_agent
39from apiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050040from oauth2client.client import Credentials
41
42
43class MockCredentials(Credentials):
44 """Mock class for all Credentials objects."""
45 def __init__(self, bearer_token):
46 super(MockCredentials, self).__init__()
47 self._authorized = 0
48 self._refreshed = 0
49 self._applied = 0
50 self._bearer_token = bearer_token
51
52 def authorize(self, http):
53 self._authorized += 1
54
55 request_orig = http.request
56
57 # The closure that will replace 'httplib2.Http.request'.
58 def new_request(uri, method='GET', body=None, headers=None,
59 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
60 connection_type=None):
61 # Modify the request headers to add the appropriate
62 # Authorization header.
63 if headers is None:
64 headers = {}
65 self.apply(headers)
66
67 resp, content = request_orig(uri, method, body, headers,
68 redirections, connection_type)
69
70 return resp, content
71
72 # Replace the request method with our own closure.
73 http.request = new_request
74
75 # Set credentials as a property of the request method.
76 setattr(http.request, 'credentials', self)
77
78 return http
79
80 def refresh(self, http):
81 self._refreshed += 1
82
83 def apply(self, headers):
84 self._applied += 1
85 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050086
87
Joe Gregoriod0bd3882011-11-22 09:49:47 -050088DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
89
90
91def datafile(filename):
92 return os.path.join(DATA_DIR, filename)
93
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050094class TestUserAgent(unittest.TestCase):
95
96 def test_set_user_agent(self):
97 http = HttpMockSequence([
98 ({'status': '200'}, 'echo_request_headers'),
99 ])
100
101 http = set_user_agent(http, "my_app/5.5")
102 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500103 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500104
105 def test_set_user_agent_nested(self):
106 http = HttpMockSequence([
107 ({'status': '200'}, 'echo_request_headers'),
108 ])
109
110 http = set_user_agent(http, "my_app/5.5")
111 http = set_user_agent(http, "my_library/0.1")
112 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500113 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500114
Joe Gregorio910b9b12012-06-12 09:36:30 -0400115
116class TestMediaUpload(unittest.TestCase):
117
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500118 def test_media_file_upload_to_from_json(self):
119 upload = MediaFileUpload(
120 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500121 self.assertEqual('image/png', upload.mimetype())
122 self.assertEqual(190, upload.size())
123 self.assertEqual(True, upload.resumable())
124 self.assertEqual(500, upload.chunksize())
125 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500126
127 json = upload.to_json()
128 new_upload = MediaUpload.new_from_json(json)
129
Joe Gregorio654f4a22012-02-09 14:15:44 -0500130 self.assertEqual('image/png', new_upload.mimetype())
131 self.assertEqual(190, new_upload.size())
132 self.assertEqual(True, new_upload.resumable())
133 self.assertEqual(500, new_upload.chunksize())
134 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500135
Ali Afshar1cb6b672012-03-12 08:46:14 -0400136 def test_media_inmemory_upload(self):
137 media = MediaInMemoryUpload('abcdef', 'text/plain', chunksize=10,
138 resumable=True)
139 self.assertEqual('text/plain', media.mimetype())
140 self.assertEqual(10, media.chunksize())
141 self.assertTrue(media.resumable())
142 self.assertEqual('bc', media.getbytes(1, 2))
143 self.assertEqual(6, media.size())
144
145 def test_media_inmemory_upload_json_roundtrip(self):
146 media = MediaInMemoryUpload(os.urandom(64), 'text/plain', chunksize=10,
147 resumable=True)
148 data = media.to_json()
149 newmedia = MediaInMemoryUpload.new_from_json(data)
150 self.assertEqual(media._body, newmedia._body)
151 self.assertEqual(media._chunksize, newmedia._chunksize)
152 self.assertEqual(media._resumable, newmedia._resumable)
153 self.assertEqual(media._mimetype, newmedia._mimetype)
154
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500155 def test_http_request_to_from_json(self):
156
157 def _postproc(*kwargs):
158 pass
159
160 http = httplib2.Http()
161 media_upload = MediaFileUpload(
162 datafile('small.png'), chunksize=500, resumable=True)
163 req = HttpRequest(
164 http,
165 _postproc,
166 'http://example.com',
167 method='POST',
168 body='{}',
169 headers={'content-type': 'multipart/related; boundary="---flubber"'},
170 methodId='foo',
171 resumable=media_upload)
172
173 json = req.to_json()
174 new_req = HttpRequest.from_json(json, http, _postproc)
175
Joe Gregorio654f4a22012-02-09 14:15:44 -0500176 self.assertEqual({'content-type':
177 'multipart/related; boundary="---flubber"'},
178 new_req.headers)
179 self.assertEqual('http://example.com', new_req.uri)
180 self.assertEqual('{}', new_req.body)
181 self.assertEqual(http, new_req.http)
182 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500183
Joe Gregorio910b9b12012-06-12 09:36:30 -0400184
185class TestMediaIoBaseUpload(unittest.TestCase):
186
187 def test_media_io_base_upload_from_file_io(self):
188 try:
189 import io
190
191 fh = io.FileIO(datafile('small.png'), 'r')
192 upload = MediaIoBaseUpload(
193 fh=fh, mimetype='image/png', chunksize=500, resumable=True)
194 self.assertEqual('image/png', upload.mimetype())
195 self.assertEqual(190, upload.size())
196 self.assertEqual(True, upload.resumable())
197 self.assertEqual(500, upload.chunksize())
198 self.assertEqual('PNG', upload.getbytes(1, 3))
199 except ImportError:
200 pass
201
202 def test_media_io_base_upload_from_file_object(self):
203 f = open(datafile('small.png'), 'r')
204 upload = MediaIoBaseUpload(
205 fh=f, mimetype='image/png', chunksize=500, resumable=True)
206 self.assertEqual('image/png', upload.mimetype())
207 self.assertEqual(190, upload.size())
208 self.assertEqual(True, upload.resumable())
209 self.assertEqual(500, upload.chunksize())
210 self.assertEqual('PNG', upload.getbytes(1, 3))
211 f.close()
212
213 def test_media_io_base_upload_serializable(self):
214 f = open(datafile('small.png'), 'r')
215 upload = MediaIoBaseUpload(fh=f, mimetype='image/png')
216
217 try:
218 json = upload.to_json()
219 self.fail('MediaIoBaseUpload should not be serializable.')
220 except NotImplementedError:
221 pass
222
223 def test_media_io_base_upload_from_string_io(self):
224 f = open(datafile('small.png'), 'r')
225 fh = StringIO.StringIO(f.read())
226 f.close()
227
228 upload = MediaIoBaseUpload(
229 fh=fh, mimetype='image/png', chunksize=500, resumable=True)
230 self.assertEqual('image/png', upload.mimetype())
231 self.assertEqual(None, upload.size())
232 self.assertEqual(True, upload.resumable())
233 self.assertEqual(500, upload.chunksize())
234 self.assertEqual('PNG', upload.getbytes(1, 3))
235 f.close()
236
237 def test_media_io_base_upload_from_bytes(self):
238 try:
239 import io
240
241 f = open(datafile('small.png'), 'r')
242 fh = io.BytesIO(f.read())
243 upload = MediaIoBaseUpload(
244 fh=fh, mimetype='image/png', chunksize=500, resumable=True)
245 self.assertEqual('image/png', upload.mimetype())
246 self.assertEqual(None, upload.size())
247 self.assertEqual(True, upload.resumable())
248 self.assertEqual(500, upload.chunksize())
249 self.assertEqual('PNG', upload.getbytes(1, 3))
250 except ImportError:
251 pass
252
253
Joe Gregorio66f57522011-11-30 11:00:00 -0500254EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
255Content-Type: application/json
256MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500257Host: www.googleapis.com
258content-length: 2\r\n\r\n{}"""
259
260
261NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
262Content-Type: application/json
263MIME-Version: 1.0
264Host: www.googleapis.com
265content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500266
267
268RESPONSE = """HTTP/1.1 200 OK
269Content-Type application/json
270Content-Length: 14
271ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
272
273
274BATCH_RESPONSE = """--batch_foobarbaz
275Content-Type: application/http
276Content-Transfer-Encoding: binary
277Content-ID: <randomness+1>
278
279HTTP/1.1 200 OK
280Content-Type application/json
281Content-Length: 14
282ETag: "etag/pony"\r\n\r\n{"foo": 42}
283
284--batch_foobarbaz
285Content-Type: application/http
286Content-Transfer-Encoding: binary
287Content-ID: <randomness+2>
288
289HTTP/1.1 200 OK
290Content-Type application/json
291Content-Length: 14
292ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
293--batch_foobarbaz--"""
294
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500295
Joe Gregorio654f4a22012-02-09 14:15:44 -0500296BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
297Content-Type: application/http
298Content-Transfer-Encoding: binary
299Content-ID: <randomness+1>
300
301HTTP/1.1 401 Authoration Required
302Content-Type application/json
303Content-Length: 14
304ETag: "etag/pony"\r\n\r\n{"error": {"message":
305 "Authorizaton failed."}}
306
307--batch_foobarbaz
308Content-Type: application/http
309Content-Transfer-Encoding: binary
310Content-ID: <randomness+2>
311
312HTTP/1.1 200 OK
313Content-Type application/json
314Content-Length: 14
315ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
316--batch_foobarbaz--"""
317
318
319BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
320Content-Type: application/http
321Content-Transfer-Encoding: binary
322Content-ID: <randomness+1>
323
324HTTP/1.1 200 OK
325Content-Type application/json
326Content-Length: 14
327ETag: "etag/pony"\r\n\r\n{"foo": 42}
328--batch_foobarbaz--"""
329
330class Callbacks(object):
331 def __init__(self):
332 self.responses = {}
333 self.exceptions = {}
334
335 def f(self, request_id, response, exception):
336 self.responses[request_id] = response
337 self.exceptions[request_id] = exception
338
339
Joe Gregorio66f57522011-11-30 11:00:00 -0500340class TestBatch(unittest.TestCase):
341
342 def setUp(self):
343 model = JsonModel()
344 self.request1 = HttpRequest(
345 None,
346 model.response,
347 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
348 method='POST',
349 body='{}',
350 headers={'content-type': 'application/json'})
351
352 self.request2 = HttpRequest(
353 None,
354 model.response,
355 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500356 method='GET',
357 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500358 headers={'content-type': 'application/json'})
359
360
361 def test_id_to_from_content_id_header(self):
362 batch = BatchHttpRequest()
363 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
364
365 def test_invalid_content_id_header(self):
366 batch = BatchHttpRequest()
367 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
368 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
369 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
370
371 def test_serialize_request(self):
372 batch = BatchHttpRequest()
373 request = HttpRequest(
374 None,
375 None,
376 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
377 method='POST',
378 body='{}',
379 headers={'content-type': 'application/json'},
380 methodId=None,
381 resumable=None)
382 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500383 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500384
Joe Gregoriodd813822012-01-25 10:32:47 -0500385 def test_serialize_request_media_body(self):
386 batch = BatchHttpRequest()
387 f = open(datafile('small.png'))
388 body = f.read()
389 f.close()
390
391 request = HttpRequest(
392 None,
393 None,
394 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
395 method='POST',
396 body=body,
397 headers={'content-type': 'application/json'},
398 methodId=None,
399 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500400 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500401 s = batch._serialize_request(request).splitlines()
402
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500403 def test_serialize_request_no_body(self):
404 batch = BatchHttpRequest()
405 request = HttpRequest(
406 None,
407 None,
408 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
409 method='POST',
410 body='',
411 headers={'content-type': 'application/json'},
412 methodId=None,
413 resumable=None)
414 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500415 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500416
Joe Gregorio66f57522011-11-30 11:00:00 -0500417 def test_deserialize_response(self):
418 batch = BatchHttpRequest()
419 resp, content = batch._deserialize_response(RESPONSE)
420
Joe Gregorio654f4a22012-02-09 14:15:44 -0500421 self.assertEqual(200, resp.status)
422 self.assertEqual('OK', resp.reason)
423 self.assertEqual(11, resp.version)
424 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500425
426 def test_new_id(self):
427 batch = BatchHttpRequest()
428
429 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500430 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500431
432 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500433 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500434
435 batch.add(self.request1, request_id='3')
436
437 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500438 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500439
440 def test_add(self):
441 batch = BatchHttpRequest()
442 batch.add(self.request1, request_id='1')
443 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
444
445 def test_add_fail_for_resumable(self):
446 batch = BatchHttpRequest()
447
448 upload = MediaFileUpload(
449 datafile('small.png'), chunksize=500, resumable=True)
450 self.request1.resumable = upload
451 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
452
453 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500454 batch = BatchHttpRequest()
455 callbacks = Callbacks()
456
457 batch.add(self.request1, callback=callbacks.f)
458 batch.add(self.request2, callback=callbacks.f)
459 http = HttpMockSequence([
460 ({'status': '200',
461 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
462 BATCH_RESPONSE),
463 ])
464 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500465 self.assertEqual({'foo': 42}, callbacks.responses['1'])
466 self.assertEqual(None, callbacks.exceptions['1'])
467 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
468 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500469
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500470 def test_execute_request_body(self):
471 batch = BatchHttpRequest()
472
473 batch.add(self.request1)
474 batch.add(self.request2)
475 http = HttpMockSequence([
476 ({'status': '200',
477 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
478 'echo_request_body'),
479 ])
480 try:
481 batch.execute(http)
482 self.fail('Should raise exception')
483 except BatchError, e:
484 boundary, _ = e.content.split(None, 1)
485 self.assertEqual('--', boundary[:2])
486 parts = e.content.split(boundary)
487 self.assertEqual(4, len(parts))
488 self.assertEqual('', parts[0])
489 self.assertEqual('--', parts[3])
490 header = parts[1].splitlines()[1]
491 self.assertEqual('Content-Type: application/http', header)
492
Joe Gregorio654f4a22012-02-09 14:15:44 -0500493 def test_execute_refresh_and_retry_on_401(self):
494 batch = BatchHttpRequest()
495 callbacks = Callbacks()
496 cred_1 = MockCredentials('Foo')
497 cred_2 = MockCredentials('Bar')
498
499 http = HttpMockSequence([
500 ({'status': '200',
501 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
502 BATCH_RESPONSE_WITH_401),
503 ({'status': '200',
504 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
505 BATCH_SINGLE_RESPONSE),
506 ])
507
508 creds_http_1 = HttpMockSequence([])
509 cred_1.authorize(creds_http_1)
510
511 creds_http_2 = HttpMockSequence([])
512 cred_2.authorize(creds_http_2)
513
514 self.request1.http = creds_http_1
515 self.request2.http = creds_http_2
516
517 batch.add(self.request1, callback=callbacks.f)
518 batch.add(self.request2, callback=callbacks.f)
519 batch.execute(http)
520
521 self.assertEqual({'foo': 42}, callbacks.responses['1'])
522 self.assertEqual(None, callbacks.exceptions['1'])
523 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
524 self.assertEqual(None, callbacks.exceptions['2'])
525
526 self.assertEqual(1, cred_1._refreshed)
527 self.assertEqual(0, cred_2._refreshed)
528
529 self.assertEqual(1, cred_1._authorized)
530 self.assertEqual(1, cred_2._authorized)
531
532 self.assertEqual(1, cred_2._applied)
533 self.assertEqual(2, cred_1._applied)
534
535 def test_http_errors_passed_to_callback(self):
536 batch = BatchHttpRequest()
537 callbacks = Callbacks()
538 cred_1 = MockCredentials('Foo')
539 cred_2 = MockCredentials('Bar')
540
541 http = HttpMockSequence([
542 ({'status': '200',
543 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
544 BATCH_RESPONSE_WITH_401),
545 ({'status': '200',
546 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
547 BATCH_RESPONSE_WITH_401),
548 ])
549
550 creds_http_1 = HttpMockSequence([])
551 cred_1.authorize(creds_http_1)
552
553 creds_http_2 = HttpMockSequence([])
554 cred_2.authorize(creds_http_2)
555
556 self.request1.http = creds_http_1
557 self.request2.http = creds_http_2
558
559 batch.add(self.request1, callback=callbacks.f)
560 batch.add(self.request2, callback=callbacks.f)
561 batch.execute(http)
562
563 self.assertEqual(None, callbacks.responses['1'])
564 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
565 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
566 self.assertEqual(None, callbacks.exceptions['2'])
567
Joe Gregorio66f57522011-11-30 11:00:00 -0500568 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500569 callbacks = Callbacks()
570 batch = BatchHttpRequest(callback=callbacks.f)
571
572 batch.add(self.request1)
573 batch.add(self.request2)
574 http = HttpMockSequence([
575 ({'status': '200',
576 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
577 BATCH_RESPONSE),
578 ])
579 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500580 self.assertEqual({'foo': 42}, callbacks.responses['1'])
581 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500582
Ali Afshar6f11ea12012-02-07 10:32:14 -0500583
584
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500585if __name__ == '__main__':
586 unittest.main()