Merge lp:~pedronis/u1db/http-more-of-simple-ops into lp:u1db

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 142
Merged at revision: 129
Proposed branch: lp:~pedronis/u1db/http-more-of-simple-ops
Merge into: lp:u1db
Diff against target: 763 lines (+336/-89)
11 files modified
u1db/backends/inmemory.py (+2/-2)
u1db/backends/sqlite_backend.py (+2/-2)
u1db/errors.py (+20/-2)
u1db/remote/http_app.py (+40/-10)
u1db/remote/http_client.py (+18/-9)
u1db/remote/http_database.py (+27/-1)
u1db/remote/http_errors.py (+34/-0)
u1db/tests/test_backends.py (+66/-60)
u1db/tests/test_http_app.py (+54/-1)
u1db/tests/test_http_client.py (+36/-2)
u1db/tests/test_http_database.py (+37/-0)
To merge this branch: bzr merge lp:~pedronis/u1db/http-more-of-simple-ops
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+83656@code.launchpad.net

Description of the change

support the various aspects of get_doc, put_doc, and delete_doc with HTTPDatabase passing the corresponding tests

introduce DocumentDoesNotExist and DocumentAlreadyDeleted to avoid overloading KeyError

improve the way we handle and transmit errors over HTTP; try to make sure all relevant wire descriptions off errors are listed in errors.py

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/28/2011 7:02 PM, Samuele Pedroni wrote:
> Samuele Pedroni has proposed merging
> lp:~pedronis/u1db/http-more-of-simple-ops into lp:u1db.
>
> Requested reviews: Ubuntu One hackers (ubuntuone-hackers)
>
> For more details, see:
> https://code.launchpad.net/~pedronis/u1db/http-more-of-simple-ops/+merge/83656
>
> support the various aspects of get_doc, put_doc, and delete_doc
> with HTTPDatabase passing the corresponding tests
>
> introduce DocumentDoesNotExist and DocumentAlreadyDeleted to avoid
> overloading KeyError
>
> improve the way we handle and transmit errors over HTTP; try to
> make sure all relevant wire descriptions off errors are listed in
> errors.py

If we are adding "wire_description = XXYY", should we be adding
"http_status = 404/409/etc"?

It seems better than hard-coding those values in the code itself.

+# wire error descriptions not corresponding to an exception
+DOCUMENT_DELETED = "document deleted"

It seems a bit odd to have DocumentAlreadyDeleted.wire_description as
one way to spell these things and DOCUMENT_DELETED as the other way.

The fact that we send the meta information for the document makes me
wonder if 404 is a good response.

...

> + except errors.HTTPError, e: + pass + +
> self.assertEqual(400, e.status)

^- I thought on some versions of python, e is only scoped inside the
except: clause. Maybe that is just python 3?

Anyway, just some thoughts:

 merge: approve

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk7Uh60ACgkQJdeBCYSNAAOY9QCgn0x3zY9vu29NfMtie89dHIdI
q+UAnR6leTGtWV5WdP3UWqiDC33ol7Bw
=IilN
-----END PGP SIGNATURE-----

review: Approve
142. By Samuele Pedroni

hardcode errors/statuses even less

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'u1db/backends/inmemory.py'
2--- u1db/backends/inmemory.py 2011-11-28 14:58:52 +0000
3+++ u1db/backends/inmemory.py 2011-11-29 10:29:24 +0000
4@@ -134,9 +134,9 @@
5
6 def delete_doc(self, doc):
7 if doc.doc_id not in self._docs:
8- raise KeyError
9+ raise errors.DocumentDoesNotExist
10 if self._docs[doc.doc_id][1] in ('null', None):
11- raise KeyError
12+ raise errors.DocumentAlreadyDeleted
13 doc.content = None
14 self.put_doc(doc)
15
16
17=== modified file 'u1db/backends/sqlite_backend.py'
18--- u1db/backends/sqlite_backend.py 2011-11-28 14:58:52 +0000
19+++ u1db/backends/sqlite_backend.py 2011-11-29 10:29:24 +0000
20@@ -325,11 +325,11 @@
21 with self._db_handle:
22 old_doc = self._get_doc(doc.doc_id)
23 if old_doc is None:
24- raise KeyError
25+ raise errors.DocumentDoesNotExist
26 if old_doc.rev != doc.rev:
27 raise errors.RevisionConflict()
28 if old_doc.content is None:
29- raise KeyError
30+ raise errors.DocumentAlreadyDeleted
31 if self._has_conflicts(doc.doc_id):
32 raise errors.ConflictedDoc()
33 new_rev = self._allocate_doc_rev(doc.rev)
34
35=== modified file 'u1db/errors.py'
36--- u1db/errors.py 2011-11-25 16:46:43 +0000
37+++ u1db/errors.py 2011-11-29 10:29:24 +0000
38@@ -46,6 +46,17 @@
39 request.
40 """
41
42+class DocumentDoesNotExist(U1DBError):
43+ """The document does not exist."""
44+
45+ wire_description="document does not exist"
46+
47+
48+class DocumentAlreadyDeleted(U1DBError):
49+ """The document was already deleted."""
50+
51+ wire_description="document already deleted"
52+
53
54 class DatabaseDoesNotExist(U1DBError):
55 """The database does not exist."""
56@@ -56,13 +67,20 @@
57
58 wire_description = None
59
60- def __init__(self, status, message=None):
61+ def __init__(self, status, message=None, headers={}):
62 self.status = status
63 self.message = message
64+ self.headers = headers
65
66
67 # mapping wire (transimission) descriptions/tags for errors to the exceptions
68 wire_description_to_exc = dict(
69 (x.wire_description, x) for x in globals().values()
70- if getattr(x, 'wire_description', None) is not None
71+ if getattr(x, 'wire_description', None) not in (None, "error")
72 )
73+wire_description_to_exc["error"] = U1DBError
74+
75+
76+#
77+# wire error descriptions not corresponding to an exception
78+DOCUMENT_DELETED = "document deleted"
79
80=== modified file 'u1db/remote/http_app.py'
81--- u1db/remote/http_app.py 2011-11-25 16:52:40 +0000
82+++ u1db/remote/http_app.py 2011-11-29 10:29:24 +0000
83@@ -28,6 +28,9 @@
84 Document,
85 errors,
86 )
87+from u1db.remote import (
88+ http_errors,
89+ )
90
91
92 class _FencedReader(object):
93@@ -126,6 +129,8 @@
94 if not (required_args <= set(args) <= all_args):
95 raise BadRequest()
96 for name, conv in conversions:
97+ if name not in args:
98+ continue
99 try:
100 args[name] = conv(args[name])
101 except ValueError:
102@@ -183,7 +188,7 @@
103 self.responder = responder
104 self.db = state.open_database(dbname)
105
106- @http_method()
107+ @http_method(old_rev=str)
108 def put(self, content, old_rev=None):
109 doc = Document(self.id, old_rev, content)
110 doc_rev = self.db.put_doc(doc)
111@@ -193,13 +198,37 @@
112 status = 200
113 self.responder.send_response(status, rev=doc_rev)
114
115+ @http_method(old_rev=str)
116+ def delete(self, old_rev=None):
117+ doc = Document(self.id, old_rev, None)
118+ self.db.delete_doc(doc)
119+ self.responder.send_response(200, rev=doc.rev)
120+
121 @http_method()
122 def get(self):
123 doc = self.db.get_doc(self.id)
124- self.responder.send_response_content(doc.content, headers={
125+ if doc is None:
126+ wire_descr = errors.DocumentDoesNotExist.wire_description
127+ self.responder.send_response(
128+ http_errors.wire_description_to_status[wire_descr],
129+ error=wire_descr,
130+ headers={
131+ 'x-u1db-rev': '',
132+ 'x-u1db-has-conflicts': 'false'
133+ })
134+ return
135+ headers={
136 'x-u1db-rev': doc.rev,
137 'x-u1db-has-conflicts': simplejson.dumps(doc.has_conflicts)
138- })
139+ }
140+ if doc.content is None:
141+ self.responder.send_response(
142+ http_errors.wire_description_to_status[errors.DOCUMENT_DELETED],
143+ error=errors.DOCUMENT_DELETED,
144+ headers=headers)
145+ else:
146+ self.responder.send_response_content(doc.content, headers=headers)
147+
148
149 @url_to_resource.register
150 class SyncResource(object):
151@@ -254,8 +283,6 @@
152 self.responder.finish_response()
153
154
155-OK = 200
156-
157 class HTTPResponder(object):
158 """Encode responses from the server back to the client."""
159
160@@ -270,7 +297,7 @@
161 self.content_type = 'application/json'
162 self.content = []
163
164- def start_response(self, status=OK, headers={}, **kwargs):
165+ def start_response(self, status=200, headers={}, **kwargs):
166 """start sending response: header and args."""
167 if self._started:
168 return
169@@ -288,7 +315,7 @@
170 """finish sending response."""
171 self.sent_response = True
172
173- def send_response(self, status=OK, headers={}, **kwargs):
174+ def send_response(self, status=200, headers={}, **kwargs):
175 """send and finish response in one go."""
176 self.start_response(status, headers, **kwargs)
177 self.finish_response()
178@@ -296,7 +323,7 @@
179 def send_response_content(self, content, headers={}):
180 """send and finish response with content in one go."""
181 headers['content-length'] = str(len(content))
182- self.start_response(OK, headers=headers)
183+ self.start_response(200, headers=headers)
184 self.content = [content]
185 self.finish_response()
186
187@@ -379,8 +406,11 @@
188 try:
189 resource = self._lookup_resource(environ, responder)
190 HTTPInvocationByMethodWithBody(resource, environ)()
191- except (errors.RevisionConflict,), e:
192- responder.send_response(409, error=e.wire_description)
193+ except errors.U1DBError, e:
194+ status = http_errors.wire_description_to_status.get(
195+ e.wire_description,
196+ 500)
197+ responder.send_response(status, error=e.wire_description)
198 except BadRequest:
199 # xxx introduce logging
200 #print environ['PATH_INFO']
201
202=== modified file 'u1db/remote/http_client.py'
203--- u1db/remote/http_client.py 2011-11-25 16:44:35 +0000
204+++ u1db/remote/http_client.py 2011-11-29 10:29:24 +0000
205@@ -22,6 +22,9 @@
206 from u1db import (
207 errors,
208 )
209+from u1db.remote import (
210+ http_errors,
211+ )
212
213
214 class HTTPClientBase(object):
215@@ -46,16 +49,22 @@
216
217 def _response(self):
218 resp = self._conn.getresponse()
219+ body = resp.read()
220+ headers = dict(resp.getheaders())
221 if resp.status in (200, 201):
222- return resp.read(), dict(resp.getheaders())
223- elif resp.status in (409,):
224- # xxx be robust against non-json response bodies
225- respdic = simplejson.loads(resp.read())
226- exc_cls = errors.wire_description_to_exc.get(respdic.get("error"))
227- if exc_cls is not None:
228- message = respdic.get("message")
229- raise exc_cls(message)
230- raise errors.HTTPError(resp.status, resp.read())
231+ return body, headers
232+ elif resp.status in http_errors.ERROR_STATUSES:
233+ try:
234+ respdic = simplejson.loads(body)
235+ except ValueError:
236+ pass
237+ else:
238+ descr = respdic.get("error")
239+ exc_cls = errors.wire_description_to_exc.get(descr)
240+ if exc_cls is not None:
241+ message = respdic.get("message")
242+ raise exc_cls(message)
243+ raise errors.HTTPError(resp.status, body, headers)
244
245 def _request(self, method, url_parts, params=None, body=None,
246 content_type=None):
247
248=== modified file 'u1db/remote/http_database.py'
249--- u1db/remote/http_database.py 2011-11-25 15:01:59 +0000
250+++ u1db/remote/http_database.py 2011-11-29 10:29:24 +0000
251@@ -20,16 +20,24 @@
252 from u1db import (
253 Database,
254 Document,
255+ errors,
256 )
257 from u1db.remote import (
258 http_client,
259+ http_errors,
260 )
261
262
263+DOCUMENT_DELETED_STATUS = http_errors.wire_description_to_status[
264+ errors.DOCUMENT_DELETED]
265+
266+
267 class HTTPDatabase(http_client.HTTPClientBase, Database):
268 """Implement the Database API to a remote HTTP server."""
269
270 def put_doc(self, doc):
271+ if doc.doc_id is None:
272+ raise errors.InvalidDocId()
273 params = {}
274 if doc.rev is not None:
275 params['old_rev'] = doc.rev
276@@ -39,7 +47,17 @@
277 return res['rev']
278
279 def get_doc(self, doc_id):
280- res, headers = self._request('GET', ['doc', doc_id])
281+ try:
282+ res, headers = self._request('GET', ['doc', doc_id])
283+ except errors.DocumentDoesNotExist:
284+ return None
285+ except errors.HTTPError, e:
286+ if (e.status == DOCUMENT_DELETED_STATUS and
287+ 'x-u1db-rev' in e.headers):
288+ res = None
289+ headers = e.headers
290+ else:
291+ raise
292 doc_rev = headers['x-u1db-rev']
293 has_conflicts = simplejson.loads(headers['x-u1db-has-conflicts'])
294 doc = Document(doc_id, doc_rev, res)
295@@ -53,3 +71,11 @@
296 content, 'application/json')
297 new_doc = Document(doc_id, res['rev'], content)
298 return new_doc
299+
300+ def delete_doc(self, doc):
301+ if doc.doc_id is None:
302+ raise errors.InvalidDocId()
303+ params = {'old_rev': doc.rev}
304+ res, headers = self._request_json('DELETE', ['doc', doc.doc_id], params)
305+ doc.content = None
306+ doc.rev = res['rev']
307
308=== added file 'u1db/remote/http_errors.py'
309--- u1db/remote/http_errors.py 1970-01-01 00:00:00 +0000
310+++ u1db/remote/http_errors.py 2011-11-29 10:29:24 +0000
311@@ -0,0 +1,34 @@
312+# Copyright 2011 Canonical Ltd.
313+#
314+# This program is free software: you can redistribute it and/or modify it
315+# under the terms of the GNU General Public License version 3, as published
316+# by the Free Software Foundation.
317+#
318+# This program is distributed in the hope that it will be useful, but
319+# WITHOUT ANY WARRANTY; without even the implied warranties of
320+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
321+# PURPOSE. See the GNU General Public License for more details.
322+#
323+# You should have received a copy of the GNU General Public License along
324+# with this program. If not, see <http://www.gnu.org/licenses/>.
325+
326+"""Information about the encoding of errors over HTTP."""
327+
328+from u1db import (
329+ errors,
330+ )
331+
332+
333+# error wire descriptions mapping to HTTP status codes
334+wire_description_to_status = dict([
335+ (errors.DocumentDoesNotExist.wire_description, 404),
336+ (errors.DocumentAlreadyDeleted.wire_description, 404),
337+ (errors.RevisionConflict.wire_description, 409),
338+# without matching exception
339+ (errors.DOCUMENT_DELETED, 404)
340+])
341+
342+
343+# 400 included for tests/anticipated usage
344+ERROR_STATUSES = set(wire_description_to_status.values())|set([400])
345+
346
347=== modified file 'u1db/tests/test_backends.py'
348--- u1db/tests/test_backends.py 2011-11-28 14:58:52 +0000
349+++ u1db/tests/test_backends.py 2011-11-29 10:29:24 +0000
350@@ -48,6 +48,9 @@
351 'server_def': http_server_def}),
352 ]
353
354+ def test_close(self):
355+ self.db.close()
356+
357 def test_create_doc_allocating_doc_id(self):
358 doc = self.db.create_doc(simple_doc)
359 self.assertNotEqual(None, doc.doc_id)
360@@ -72,11 +75,15 @@
361 new_rev = self.db.put_doc(doc)
362 self.assertGetDoc(self.db, 'my_doc_id', new_rev, simple_doc, False)
363
364-
365-class LocalDatabaseTests(tests.DatabaseBaseTests):
366-
367- def test_close(self):
368- self.db.close()
369+ def test_put_doc_update(self):
370+ doc = self.db.create_doc(simple_doc, doc_id='my_doc_id')
371+ orig_rev = doc.rev
372+ doc.content = '{"updated": "stuff"}'
373+ new_rev = self.db.put_doc(doc)
374+ self.assertNotEqual(new_rev, orig_rev)
375+ self.assertGetDoc(self.db, 'my_doc_id', new_rev,
376+ '{"updated": "stuff"}', False)
377+ self.assertEqual(doc.rev, new_rev)
378
379 def test_put_doc_refuses_no_id(self):
380 doc = Document(None, None, simple_doc)
381@@ -86,6 +93,60 @@
382 doc = Document('doc-id', 'test:4', simple_doc)
383 self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc)
384
385+ def test_put_fails_with_bad_old_rev(self):
386+ doc = self.db.create_doc(simple_doc, doc_id='my_doc_id')
387+ old_rev = doc.rev
388+ doc.rev = 'other:1'
389+ doc.content = '{"something": "else"}'
390+ self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc)
391+ self.assertGetDoc(self.db, 'my_doc_id', old_rev, simple_doc, False)
392+
393+ def test_get_doc_after_put(self):
394+ doc = self.db.create_doc(simple_doc, doc_id='my_doc_id')
395+ self.assertGetDoc(self.db, 'my_doc_id', doc.rev, simple_doc, False)
396+
397+ def test_get_doc_nonexisting(self):
398+ self.assertIs(None, self.db.get_doc('non-existing'))
399+
400+ def test_handles_nested_content(self):
401+ doc = self.db.create_doc(nested_doc)
402+ self.assertGetDoc(self.db, doc.doc_id, doc.rev, nested_doc, False)
403+
404+ def test_handles_doc_with_null(self):
405+ doc = self.db.create_doc('{"key": null}')
406+ self.assertGetDoc(self.db, doc.doc_id, doc.rev, '{"key": null}', False)
407+
408+ def test_delete_doc(self):
409+ doc = self.db.create_doc(simple_doc)
410+ self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False)
411+ orig_rev = doc.rev
412+ self.db.delete_doc(doc)
413+ self.assertNotEqual(orig_rev, doc.rev)
414+ self.assertGetDoc(self.db, doc.doc_id, doc.rev, None, False)
415+ self.assertIsNot(None, self.db.get_doc(doc.doc_id))
416+
417+ def test_delete_doc_non_existant(self):
418+ doc = Document('non-existing', 'other:1', simple_doc)
419+ self.assertRaises(errors.DocumentDoesNotExist,
420+ self.db.delete_doc, doc)
421+
422+ def test_delete_doc_already_deleted(self):
423+ doc = self.db.create_doc(simple_doc)
424+ self.db.delete_doc(doc)
425+ self.assertRaises(errors.DocumentAlreadyDeleted,
426+ self.db.delete_doc, doc)
427+ self.assertGetDoc(self.db, doc.doc_id, doc.rev, None, False)
428+
429+ def test_delete_doc_bad_rev(self):
430+ doc1 = self.db.create_doc(simple_doc)
431+ self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False)
432+ doc2 = Document(doc1.doc_id, 'other:1', simple_doc)
433+ self.assertRaises(errors.RevisionConflict, self.db.delete_doc, doc2)
434+ self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False)
435+
436+
437+class LocalDatabaseTests(tests.DatabaseBaseTests):
438+
439 def test_get_docs(self):
440 doc1 = self.db.create_doc(simple_doc)
441 doc2 = self.db.create_doc(nested_doc)
442@@ -183,11 +244,6 @@
443 def test_get_docs_empty_list(self):
444 self.assertEqual([], self.db.get_docs([]))
445
446- def test_put_doc_creating_initial(self):
447- doc = Document('my_doc_id', None, simple_doc)
448- new_rev = self.db.put_doc(doc)
449- self.assertGetDoc(self.db, 'my_doc_id', new_rev, simple_doc, False)
450-
451 def test_simple_put_doc_if_newer(self):
452 doc = Document('my-doc-id', 'test:1', simple_doc)
453 state = self.db.put_doc_if_newer(doc)
454@@ -231,53 +287,11 @@
455 (doc1.rev, simple_doc)],
456 self.db.get_doc_conflicts(doc1.doc_id))
457
458- def test_get_doc_after_put(self):
459- doc = self.db.create_doc(simple_doc, doc_id='my_doc_id')
460- self.assertGetDoc(self.db, 'my_doc_id', doc.rev, simple_doc, False)
461-
462- def test_get_doc_nonexisting(self):
463- self.assertIs(None, self.db.get_doc('non-existing'))
464-
465 def test_get_sync_generation(self):
466 self.assertEqual(0, self.db.get_sync_generation('other-db'))
467 self.db.set_sync_generation('other-db', 2)
468 self.assertEqual(2, self.db.get_sync_generation('other-db'))
469
470- def test_put_fails_with_bad_old_rev(self):
471- doc = self.db.create_doc(simple_doc, doc_id='my_doc_id')
472- old_rev = doc.rev
473- doc.rev = 'other:1'
474- doc.content = '{"something": "else"}'
475- self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc)
476- self.assertGetDoc(self.db, 'my_doc_id', old_rev, simple_doc, False)
477-
478- def test_delete_doc(self):
479- doc = self.db.create_doc(simple_doc)
480- self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False)
481- orig_rev = doc.rev
482- self.db.delete_doc(doc)
483- self.assertNotEqual(orig_rev, doc.rev)
484- self.assertGetDoc(self.db, doc.doc_id, doc.rev, None, False)
485- self.assertIsNot(None, self.db.get_doc(doc.doc_id))
486-
487- def test_delete_doc_non_existant(self):
488- doc = Document('non-existing', 'other:1', simple_doc)
489- self.assertRaises(KeyError,
490- self.db.delete_doc, doc)
491-
492- def test_delete_doc_already_deleted(self):
493- doc = self.db.create_doc(simple_doc)
494- self.db.delete_doc(doc)
495- self.assertRaises(KeyError, self.db.delete_doc, doc)
496- self.assertGetDoc(self.db, doc.doc_id, doc.rev, None, False)
497-
498- def test_delete_doc_bad_rev(self):
499- doc1 = self.db.create_doc(simple_doc)
500- self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False)
501- doc2 = Document(doc1.doc_id, 'other:1', simple_doc)
502- self.assertRaises(errors.RevisionConflict, self.db.delete_doc, doc2)
503- self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False)
504-
505 def test_put_updates_transaction_log(self):
506 doc = self.db.create_doc(simple_doc)
507 self.assertEqual([doc.doc_id], self.db._get_transaction_log())
508@@ -303,14 +317,6 @@
509 self.assertEqual((2, set([doc.doc_id])), self.db.whats_changed())
510 self.assertEqual((2, set()), self.db.whats_changed(2))
511
512- def test_handles_nested_content(self):
513- doc = self.db.create_doc(nested_doc)
514- self.assertGetDoc(self.db, doc.doc_id, doc.rev, nested_doc, False)
515-
516- def test_handles_doc_with_null(self):
517- doc = self.db.create_doc('{"key": null}')
518- self.assertGetDoc(self.db, doc.doc_id, doc.rev, '{"key": null}', False)
519-
520
521 class DatabaseIndexTests(tests.DatabaseBaseTests):
522
523
524=== modified file 'u1db/tests/test_http_app.py'
525--- u1db/tests/test_http_app.py 2011-11-25 15:01:59 +0000
526+++ u1db/tests/test_http_app.py 2011-11-29 10:29:24 +0000
527@@ -26,6 +26,7 @@
528
529 from u1db.remote import (
530 http_app,
531+ http_errors,
532 )
533
534
535@@ -160,6 +161,13 @@
536 self.assertRaises(http_app.BadRequest, f, "self",
537 {"a": "x", "b": "foo"}, None)
538
539+ def test_args_conversion_with_default(self):
540+ @http_app.http_method(b=str)
541+ def f(self, a, b=None):
542+ return self, a, b
543+ res = f("self", {"a": "x"}, None)
544+ self.assertEqual(("self", "x", None), res)
545+
546 def test_args_content(self):
547 @http_app.http_method()
548 def f(self, a, content):
549@@ -432,6 +440,15 @@
550 self.assertEqual('application/json', resp.header('content-type'))
551 self.assertEqual({'rev': doc.rev}, simplejson.loads(resp.body))
552
553+ def test_delete_doc(self):
554+ doc = self.db0.create_doc('{"x": 1}', doc_id='doc1')
555+ resp = self.app.delete('/db0/doc/doc1?old_rev=%s' % doc.rev)
556+ doc = self.db0.get_doc('doc1')
557+ self.assertEqual(None, doc.content)
558+ self.assertEqual(200, resp.status)
559+ self.assertEqual('application/json', resp.header('content-type'))
560+ self.assertEqual({'rev': doc.rev}, simplejson.loads(resp.body))
561+
562 def test_get_doc(self):
563 doc = self.db0.create_doc('{"x": 1}', doc_id='doc1')
564 resp = self.app.get('/db0/doc/%s' % doc.doc_id)
565@@ -441,6 +458,26 @@
566 self.assertEqual(doc.rev, resp.header('x-u1db-rev'))
567 self.assertEqual('false', resp.header('x-u1db-has-conflicts'))
568
569+ def test_get_doc_non_existing(self):
570+ resp = self.app.get('/db0/doc/not-there', expect_errors=True)
571+ self.assertEqual(404, resp.status)
572+ self.assertEqual('application/json', resp.header('content-type'))
573+ self.assertEqual({"error": "document does not exist"},
574+ simplejson.loads(resp.body))
575+ self.assertEqual('', resp.header('x-u1db-rev'))
576+ self.assertEqual('false', resp.header('x-u1db-has-conflicts'))
577+
578+ def test_get_doc_deleted(self):
579+ doc = self.db0.create_doc('{"x": 1}', doc_id='doc1')
580+ self.db0.delete_doc(doc)
581+ resp = self.app.get('/db0/doc/doc1', expect_errors=True)
582+ self.assertEqual(404, resp.status)
583+ self.assertEqual('application/json', resp.header('content-type'))
584+ self.assertEqual({"error": errors.DOCUMENT_DELETED},
585+ simplejson.loads(resp.body))
586+ self.assertEqual(doc.rev, resp.header('x-u1db-rev'))
587+ self.assertEqual('false', resp.header('x-u1db-has-conflicts'))
588+
589 def test_get_sync_info(self):
590 self.db0.set_sync_generation('other-id', 1)
591 resp = self.app.get('/db0/sync-from/other-id')
592@@ -497,6 +534,12 @@
593 simplejson.loads(parts[1]))
594
595
596+class TestHTTPErrors(tests.TestCase):
597+
598+ def test_wire_description_to_status(self):
599+ self.assertNotIn("error", http_errors.wire_description_to_status)
600+
601+
602 class TestHTTPAppErrorHandling(tests.TestCase):
603
604 def setUp(self):
605@@ -515,7 +558,7 @@
606 application._lookup_resource = lookup_resource
607 self.app = paste.fixture.TestApp(application)
608
609- def test_RevisionConflict(self):
610+ def test_RevisionConflict_etc(self):
611 self.exc = errors.RevisionConflict()
612 resp = self.app.post('/req', params='{}',
613 headers={'content-type': 'application/json'},
614@@ -524,3 +567,13 @@
615 self.assertEqual('application/json', resp.header('content-type'))
616 self.assertEqual({"error": "revision conflict"},
617 simplejson.loads(resp.body))
618+
619+ def test_generic_u1db_errors(self):
620+ self.exc = errors.U1DBError()
621+ resp = self.app.post('/req', params='{}',
622+ headers={'content-type': 'application/json'},
623+ expect_errors=True)
624+ self.assertEqual(500, resp.status)
625+ self.assertEqual('application/json', resp.header('content-type'))
626+ self.assertEqual({"error": "error"},
627+ simplejson.loads(resp.body))
628
629=== modified file 'u1db/tests/test_http_client.py'
630--- u1db/tests/test_http_client.py 2011-11-25 14:48:06 +0000
631+++ u1db/tests/test_http_client.py 2011-11-29 10:29:24 +0000
632@@ -42,12 +42,14 @@
633 elif environ['PATH_INFO'].endswith('error'):
634 content_length = int(environ['CONTENT_LENGTH'])
635 error = simplejson.loads(environ['wsgi.input'].read(content_length))
636- start_response(error['status'],
637- [('Content-Type', 'application/json')])
638 response = error['response']
639 if isinstance(response, basestring):
640+ start_response(error['status'],
641+ [('Content-Type', 'text/plain')])
642 return [response]
643 else:
644+ start_response(error['status'],
645+ [('Content-Type', 'application/json')])
646 return [simplejson.dumps(error['response'])]
647
648 def server_def(self):
649@@ -142,6 +144,7 @@
650
651 self.assertEqual(500, e.status)
652 self.assertEqual("Fail.", e.message)
653+ self.assertTrue("content-type" in e.headers)
654
655 def test_revision_conflict(self):
656 cli = self.getClient()
657@@ -149,3 +152,34 @@
658 cli._request_json, 'POST', ['error'], {},
659 {'status': "409 Conflict",
660 'response': {"error": "revision conflict"}})
661+
662+ def test_generic_u1db_error(self):
663+ cli = self.getClient()
664+ self.assertRaises(errors.U1DBError,
665+ cli._request_json, 'POST', ['error'], {},
666+ {'status': "400 Bad Request",
667+ 'response': {"error": "error"}})
668+ try:
669+ cli._request_json('POST', ['error'], {},
670+ {'status': "400 Bad Request",
671+ 'response': {"error": "error"}})
672+ except errors.U1DBError, e:
673+ pass
674+ self.assertIs(e.__class__, errors.U1DBError)
675+
676+ def test_unspecified_bad_request(self):
677+ cli = self.getClient()
678+ self.assertRaises(errors.HTTPError,
679+ cli._request_json, 'POST', ['error'], {},
680+ {'status': "400 Bad Request",
681+ 'response': "<Bad Request>"})
682+ try:
683+ cli._request_json('POST', ['error'], {},
684+ {'status': "400 Bad Request",
685+ 'response': "<Bad Request>"})
686+ except errors.HTTPError, e:
687+ pass
688+
689+ self.assertEqual(400, e.status)
690+ self.assertEqual("<Bad Request>", e.message)
691+ self.assertTrue("content-type" in e.headers)
692
693=== modified file 'u1db/tests/test_http_database.py'
694--- u1db/tests/test_http_database.py 2011-11-25 15:01:59 +0000
695+++ u1db/tests/test_http_database.py 2011-11-29 10:29:24 +0000
696@@ -15,8 +15,10 @@
697 """Tests for HTTPDatabase"""
698
699 import inspect
700+import simplejson
701
702 from u1db import (
703+ errors,
704 Document,
705 tests,
706 )
707@@ -36,10 +38,14 @@
708 def _request(method, url_parts, params=None, body=None,
709 content_type=None):
710 self.got = method, url_parts, params, body, content_type
711+ if isinstance(self.response_val, Exception):
712+ raise self.response_val
713 return self.response_val
714 def _request_json(method, url_parts, params=None, body=None,
715 content_type=None):
716 self.got = method, url_parts, params, body, content_type
717+ if isinstance(self.response_val, Exception):
718+ raise self.response_val
719 return self.response_val
720 self.db._request = _request
721 self.db._request_json = _request_json
722@@ -79,6 +85,29 @@
723 self.assertEqual(('GET', ['doc', 'doc-id'], None, None, None),
724 self.got)
725
726+ def test_get_doc_non_existing(self):
727+ self.response_val = errors.DocumentDoesNotExist()
728+ self.assertIs(None, self.db.get_doc('not-there'))
729+ self.assertEqual(('GET', ['doc', 'not-there'], None, None, None),
730+ self.got)
731+
732+ def test_get_doc_deleted(self):
733+ self.response_val = errors.HTTPError(404,
734+ simplejson.dumps(
735+ {"error": errors.DOCUMENT_DELETED}
736+ ),
737+ {'x-u1db-rev': 'doc-rev-gone',
738+ 'x-u1db-has-conflicts': 'false'})
739+ doc = self.db.get_doc('deleted')
740+ self.assertEqual('deleted', doc.doc_id)
741+ self.assertEqual('doc-rev-gone', doc.rev)
742+ self.assertIs(None, doc.content)
743+
744+ def test_get_doc_pass_through_errors(self):
745+ self.response_val = errors.HTTPError(500, 'Crash.')
746+ self.assertRaises(errors.HTTPError,
747+ self.db.get_doc, 'something-something')
748+
749 def test_create_doc_with_id(self):
750 self.response_val = {'rev': 'doc-rev'}, {}
751 new_doc = self.db.create_doc('{"v": 1}', doc_id='doc-id')
752@@ -95,3 +124,11 @@
753 self.assertEqual('{"v": 3}', new_doc.content)
754 self.assertEqual(('PUT', ['doc', new_doc.doc_id], {},
755 '{"v": 3}', 'application/json'), self.got)
756+
757+ def test_delete_doc(self):
758+ self.response_val = {'rev': 'doc-rev-gone'}, {}
759+ doc = Document('doc-id', 'doc-rev', None)
760+ self.db.delete_doc(doc)
761+ self.assertEqual('doc-rev-gone', doc.rev)
762+ self.assertEqual(('DELETE', ['doc', 'doc-id'], {'old_rev': 'doc-rev'},
763+ None, None), self.got)

Subscribers

People subscribed via source and target branches