Merge lp:~pedronis/u1db/http-more-of-simple-ops into lp:u1db
- http-more-of-simple-ops
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel (community) | Approve | ||
Review via email: mp+83656@code.launchpad.net |
Commit message
Description of the change
support the various aspects of get_doc, put_doc, and delete_doc with HTTPDatabase passing the corresponding tests
introduce DocumentDoesNot
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.
- 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) |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11/28/2011 7:02 PM, Samuele Pedroni wrote: /code.launchpad .net/~pedronis/ u1db/http- more-of- simple- ops/+merge/ 83656 Exist and DocumentAlready Deleted to avoid
> 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:/
>
> support the various aspects of get_doc, put_doc, and delete_doc
> with HTTPDatabase passing the corresponding tests
>
> introduce DocumentDoesNot
> 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 DocumentAlready Deleted. wire_descriptio n 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 + + l(400, e.status)
> self.assertEqua
^- 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 enigmail. mozdev. org/
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk7 Uh60ACgkQJdeBCY SNAAOY9QCgn0x3z Y9vu29NfMtie89d HIdI WdP3UWqiDC33ol7 Bw
q+UAnR6leTGtWV5
=IilN
-----END PGP SIGNATURE-----