Merge lp:~benji/lazr.restful/tweak-etag into lp:lazr.restful
- tweak-etag
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Paul Hummer |
Approved revision: | 155 |
Merged at revision: | 151 |
Proposed branch: | lp:~benji/lazr.restful/tweak-etag |
Merge into: | lp:lazr.restful |
Diff against target: |
527 lines (+347/-51) 6 files modified
src/lazr/restful/_resource.py (+61/-48) src/lazr/restful/testing/helpers.py (+12/-0) src/lazr/restful/tests/test_etag.py (+253/-0) src/lazr/restful/tests/test_utils.py (+15/-3) src/lazr/restful/utils.py (+5/-0) versions.cfg (+1/-0) |
To merge this branch: | bzr merge lp:~benji/lazr.restful/tweak-etag |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | Approve | ||
Review via email: mp+37194@code.launchpad.net |
Commit message
Description of the change
The ETag generation in lazr.restful was a bit too conservative. It used the revision number in every ETag when it is only needed in a subset. This branch moves the inclusion of revno "down" into the subclasses that need it. Tests for the various ETag generating method/functions were added as well as tests for the (existing) read/write ETag checking routines.
Leonard and I had pre-, intra-, and post-implementation discussions.
Paul Hummer (rockstar) : | # |
Stuart Bishop (stub) wrote : | # |
Benji York (benji) wrote : | # |
On Thu, Sep 30, 2010 at 11:15 PM, Stuart Bishop
<email address hidden> wrote:
> I notice you are adding a roman numeral package to buildout, but are not using it.
That's a change from another branch that I'll probably be reverting. I
think I got a hold of a bad docutils somehow that was missing roman.
--
Benji York
Leonard Richardson (leonardr) wrote : | # |
I have some minor comments, but overall this is a good branch.
In addition to the 'roman' package, you've got a normalizer that makes Unicode strings look the same as regular strings. I don't think that's the best way to solve your problem, but you should talk it over with Gary.
make_entry_
Similarly, you don't need "modifiable" in test_writable_
Do you want to add a test to test_cores_
Benji York (benji) wrote : | # |
I fixed the roman problem by nuking my docutils eggs and reinstalling.
- 156. By Benji York <benji@benji-laptop>
-
remove unneeded reNormalizer
Benji York (benji) wrote : | # |
I made all the changes Leonard suggested except for the last one which we discussed away.
Preview Diff
1 | === modified file 'src/lazr/restful/_resource.py' | |||
2 | --- src/lazr/restful/_resource.py 2010-09-23 19:14:55 +0000 | |||
3 | +++ src/lazr/restful/_resource.py 2010-10-04 15:46:40 +0000 | |||
4 | @@ -89,7 +89,10 @@ | |||
5 | 89 | IWebServiceConfiguration, IWebServiceLayer, IWebServiceVersion, | 89 | IWebServiceConfiguration, IWebServiceLayer, IWebServiceVersion, |
6 | 90 | LAZR_WEBSERVICE_NAME) | 90 | LAZR_WEBSERVICE_NAME) |
7 | 91 | from lazr.restful.utils import ( | 91 | from lazr.restful.utils import ( |
9 | 92 | get_current_web_service_request, sorted_named_things) | 92 | extract_write_portion, |
10 | 93 | get_current_web_service_request, | ||
11 | 94 | sorted_named_things, | ||
12 | 95 | ) | ||
13 | 93 | 96 | ||
14 | 94 | 97 | ||
15 | 95 | # The path to the WADL XML Schema definition. | 98 | # The path to the WADL XML Schema definition. |
16 | @@ -378,12 +381,6 @@ | |||
17 | 378 | # different representations of a resource interchangeably. | 381 | # different representations of a resource interchangeably. |
18 | 379 | core_hashes[-1].update("\0" + media_type) | 382 | core_hashes[-1].update("\0" + media_type) |
19 | 380 | 383 | ||
20 | 381 | # Append the revision number, because the algorithm for | ||
21 | 382 | # generating the representation might itself change across | ||
22 | 383 | # versions. | ||
23 | 384 | revno = getUtility(IWebServiceConfiguration).code_revision | ||
24 | 385 | core_hashes[-1].update("\0" + revno) | ||
25 | 386 | |||
26 | 387 | etag = '"%s"' % "-".join([core.hexdigest() for core in core_hashes]) | 384 | etag = '"%s"' % "-".join([core.hexdigest() for core in core_hashes]) |
27 | 388 | self.etags_by_media_type[media_type] = etag | 385 | self.etags_by_media_type[media_type] = etag |
28 | 389 | return etag | 386 | return etag |
29 | @@ -1255,14 +1252,21 @@ | |||
30 | 1255 | 1252 | ||
31 | 1256 | do_PATCH = do_PUT | 1253 | do_PATCH = do_PUT |
32 | 1257 | 1254 | ||
34 | 1258 | def _getETagCores(self, unmarshalled_field_values=None): | 1255 | def _getETagCores(self, cache=None): |
35 | 1259 | """Calculate the ETag for an entry field. | 1256 | """Calculate the ETag for an entry field. |
36 | 1260 | 1257 | ||
37 | 1261 | The core of the ETag is the field value itself. | 1258 | The core of the ETag is the field value itself. |
38 | 1259 | |||
39 | 1260 | :arg cache: is ignored. | ||
40 | 1262 | """ | 1261 | """ |
44 | 1263 | name, value = self._unmarshallField( | 1262 | value = self._unmarshallField( |
45 | 1264 | self.context.name, self.context.field) | 1263 | self.context.name, self.context.field)[1] |
46 | 1265 | return [str(value)] | 1264 | |
47 | 1265 | # Append the revision number, because the algorithm for | ||
48 | 1266 | # generating the representation might itself change across | ||
49 | 1267 | # versions. | ||
50 | 1268 | revno = getUtility(IWebServiceConfiguration).code_revision | ||
51 | 1269 | return [core.encode('utf-8') for core in [revno, unicode(value)]] | ||
52 | 1266 | 1270 | ||
53 | 1267 | def _representation(self, media_type): | 1271 | def _representation(self, media_type): |
54 | 1268 | """Create a representation of the field value.""" | 1272 | """Create a representation of the field value.""" |
55 | @@ -1304,6 +1308,29 @@ | |||
56 | 1304 | self.context = entryfield | 1308 | self.context = entryfield |
57 | 1305 | self.request = request | 1309 | self.request = request |
58 | 1306 | 1310 | ||
59 | 1311 | def make_entry_etag_cores(field_details): | ||
60 | 1312 | """Given the details of an entry's fields, calculate its ETag cores. | ||
61 | 1313 | |||
62 | 1314 | :arg field_details: A list of field names and relevant field information, | ||
63 | 1315 | in particular whether or not the field is writable and its current value. | ||
64 | 1316 | """ | ||
65 | 1317 | unwritable_values = [] | ||
66 | 1318 | writable_values = [] | ||
67 | 1319 | for name, details in field_details: | ||
68 | 1320 | if details['writable']: | ||
69 | 1321 | # The client can write to this value. | ||
70 | 1322 | bucket = writable_values | ||
71 | 1323 | else: | ||
72 | 1324 | # The client can't write to this value (it might be read-only or | ||
73 | 1325 | # it might just be non-web-service writable. | ||
74 | 1326 | bucket = unwritable_values | ||
75 | 1327 | bucket.append(decode_value(details['value'])) | ||
76 | 1328 | |||
77 | 1329 | unwritable = "\0".join(unwritable_values).encode("utf-8") | ||
78 | 1330 | writable = "\0".join(writable_values).encode("utf-8") | ||
79 | 1331 | return [unwritable, writable] | ||
80 | 1332 | |||
81 | 1333 | |||
82 | 1307 | 1334 | ||
83 | 1308 | class EntryResource(CustomOperationResourceMixin, | 1335 | class EntryResource(CustomOperationResourceMixin, |
84 | 1309 | FieldUnmarshallerMixin, EntryManipulatingResource): | 1336 | FieldUnmarshallerMixin, EntryManipulatingResource): |
85 | @@ -1338,31 +1365,23 @@ | |||
86 | 1338 | unmarshalled values, obtained during some other operation such | 1365 | unmarshalled values, obtained during some other operation such |
87 | 1339 | as the construction of a representation. | 1366 | as the construction of a representation. |
88 | 1340 | """ | 1367 | """ |
91 | 1341 | unwritable_values = [] | 1368 | if unmarshalled_field_values is None: |
92 | 1342 | writable_values = [] | 1369 | unmarshalled_field_values = {} |
93 | 1370 | |||
94 | 1371 | field_details = [] | ||
95 | 1343 | for name, field in getFieldsInOrder(self.entry.schema): | 1372 | for name, field in getFieldsInOrder(self.entry.schema): |
118 | 1344 | if self.isModifiableField(field, True): | 1373 | details = {} |
119 | 1345 | # The client can write to this value. | 1374 | # Add in any values provided by the caller. |
120 | 1346 | bucket = writable_values | 1375 | # The value of the field is either passed in, or extracted. |
121 | 1347 | elif self.isModifiableField(field, False): | 1376 | details['value'] = unmarshalled_field_values.get( |
122 | 1348 | # The client can't write this value, but it still | 1377 | name, self._unmarshallField(name, field)[1]) |
123 | 1349 | # might change. | 1378 | |
124 | 1350 | bucket = unwritable_values | 1379 | # The client can write to this field. |
125 | 1351 | else: | 1380 | details['writable'] = self.isModifiableField(field, True) |
126 | 1352 | # This value can never change, and as such does not | 1381 | |
127 | 1353 | # need to be included in the ETag. | 1382 | field_details.append((name, details)) |
128 | 1354 | continue | 1383 | |
129 | 1355 | if (unmarshalled_field_values is not None | 1384 | return make_entry_etag_cores(field_details) |
108 | 1356 | and unmarshalled_field_values.get(name)): | ||
109 | 1357 | value = unmarshalled_field_values[name] | ||
110 | 1358 | else: | ||
111 | 1359 | ignored, value = self._unmarshallField(name, field) | ||
112 | 1360 | bucket.append(decode_value(value)) | ||
113 | 1361 | |||
114 | 1362 | unwritable = "\0".join(unwritable_values).encode("utf-8") | ||
115 | 1363 | writable = "\0".join(writable_values).encode("utf-8") | ||
116 | 1364 | return [unwritable, writable] | ||
117 | 1365 | |||
130 | 1366 | 1385 | ||
131 | 1367 | def _etagMatchesForWrite(self, existing_etag, incoming_etags): | 1386 | def _etagMatchesForWrite(self, existing_etag, incoming_etags): |
132 | 1368 | """Make sure no other client has modified this resource. | 1387 | """Make sure no other client has modified this resource. |
133 | @@ -1373,15 +1392,9 @@ | |||
134 | 1373 | on conditional writes where the only fields that changed are | 1392 | on conditional writes where the only fields that changed are |
135 | 1374 | read-only fields that can't possibly cause a conflict. | 1393 | read-only fields that can't possibly cause a conflict. |
136 | 1375 | """ | 1394 | """ |
146 | 1376 | existing_write_portion = existing_etag.split('-', 1)[-1] | 1395 | incoming_write_portions = map(extract_write_portion, incoming_etags) |
147 | 1377 | for etag in incoming_etags: | 1396 | existing_write_portion = extract_write_portion(existing_etag) |
148 | 1378 | if '-' in etag: | 1397 | return existing_write_portion in incoming_write_portions |
140 | 1379 | incoming_write_portion = etag.rsplit('-', 1)[-1] | ||
141 | 1380 | else: | ||
142 | 1381 | incoming_write_portion = etag | ||
143 | 1382 | if existing_write_portion == incoming_write_portion: | ||
144 | 1383 | return True | ||
145 | 1384 | return False | ||
149 | 1385 | 1398 | ||
150 | 1386 | 1399 | ||
151 | 1387 | def toDataForJSON(self): | 1400 | def toDataForJSON(self): |
152 | @@ -1412,7 +1425,7 @@ | |||
153 | 1412 | "Cannot create data structure for media type %s" | 1425 | "Cannot create data structure for media type %s" |
154 | 1413 | % media_type) | 1426 | % media_type) |
155 | 1414 | data[repr_name] = repr_value | 1427 | data[repr_name] = repr_value |
157 | 1415 | unmarshalled_field_values[name] = repr_value | 1428 | unmarshalled_field_values[name] = repr_value |
158 | 1416 | 1429 | ||
159 | 1417 | etag = self.getETag(media_type, unmarshalled_field_values) | 1430 | etag = self.getETag(media_type, unmarshalled_field_values) |
160 | 1418 | data['http_etag'] = etag | 1431 | data['http_etag'] = etag |
161 | @@ -1778,10 +1791,10 @@ | |||
162 | 1778 | """Calculate an ETag for a representation of this resource. | 1791 | """Calculate an ETag for a representation of this resource. |
163 | 1779 | 1792 | ||
164 | 1780 | The service root resource changes only when the software | 1793 | The service root resource changes only when the software |
167 | 1781 | itself changes. This information goes into the ETag already, | 1794 | itself changes. |
166 | 1782 | so there's no need to provide anything. | ||
168 | 1783 | """ | 1795 | """ |
170 | 1784 | return [''] | 1796 | revno = getUtility(IWebServiceConfiguration).code_revision |
171 | 1797 | return [revno.encode('utf-8')] | ||
172 | 1785 | 1798 | ||
173 | 1786 | def __call__(self, REQUEST=None): | 1799 | def __call__(self, REQUEST=None): |
174 | 1787 | """Handle a GET request.""" | 1800 | """Handle a GET request.""" |
175 | 1788 | 1801 | ||
176 | === modified file 'src/lazr/restful/testing/helpers.py' | |||
177 | --- src/lazr/restful/testing/helpers.py 2010-07-15 14:24:56 +0000 | |||
178 | +++ src/lazr/restful/testing/helpers.py 2010-10-04 15:46:40 +0000 | |||
179 | @@ -5,6 +5,12 @@ | |||
180 | 5 | from zope.interface import implements | 5 | from zope.interface import implements |
181 | 6 | 6 | ||
182 | 7 | from lazr.restful.interfaces import IWebServiceConfiguration | 7 | from lazr.restful.interfaces import IWebServiceConfiguration |
183 | 8 | from lazr.restful.simple import ( | ||
184 | 9 | Request, | ||
185 | 10 | RootResource, | ||
186 | 11 | ) | ||
187 | 12 | from lazr.restful.testing.webservice import WebServiceTestPublication | ||
188 | 13 | from lazr.restful.utils import tag_request_with_version_name | ||
189 | 8 | 14 | ||
190 | 9 | 15 | ||
191 | 10 | def create_test_module(name, *contents): | 16 | def create_test_module(name, *contents): |
192 | @@ -40,6 +46,12 @@ | |||
193 | 40 | last_version_with_mutator_named_operations = "1.0" | 46 | last_version_with_mutator_named_operations = "1.0" |
194 | 41 | code_revision = "1.0b" | 47 | code_revision = "1.0b" |
195 | 42 | default_batch_size = 50 | 48 | default_batch_size = 50 |
196 | 49 | hostname = 'example.com' | ||
197 | 43 | 50 | ||
198 | 44 | def get_request_user(self): | 51 | def get_request_user(self): |
199 | 45 | return 'A user' | 52 | return 'A user' |
200 | 53 | |||
201 | 54 | def createRequest(self, body_instream, environ): | ||
202 | 55 | request = Request(body_instream, environ) | ||
203 | 56 | request.setPublication(WebServiceTestPublication(RootResource)) | ||
204 | 57 | return request | ||
205 | 46 | 58 | ||
206 | === added file 'src/lazr/restful/tests/test_etag.py' | |||
207 | --- src/lazr/restful/tests/test_etag.py 1970-01-01 00:00:00 +0000 | |||
208 | +++ src/lazr/restful/tests/test_etag.py 2010-10-04 15:46:40 +0000 | |||
209 | @@ -0,0 +1,253 @@ | |||
210 | 1 | # Copyright 2008 Canonical Ltd. All rights reserved. | ||
211 | 2 | """Tests for ETag generation.""" | ||
212 | 3 | |||
213 | 4 | __metaclass__ = type | ||
214 | 5 | |||
215 | 6 | import unittest | ||
216 | 7 | |||
217 | 8 | from zope.component import provideUtility | ||
218 | 9 | |||
219 | 10 | from lazr.restful.interfaces import IWebServiceConfiguration | ||
220 | 11 | from lazr.restful.testing.helpers import TestWebServiceConfiguration | ||
221 | 12 | from lazr.restful.testing.webservice import create_web_service_request | ||
222 | 13 | from lazr.restful._resource import ( | ||
223 | 14 | EntryFieldResource, | ||
224 | 15 | EntryResource, | ||
225 | 16 | HTTPResource, | ||
226 | 17 | ServiceRootResource, | ||
227 | 18 | make_entry_etag_cores, | ||
228 | 19 | ) | ||
229 | 20 | |||
230 | 21 | |||
231 | 22 | class TestEntryResourceETags(unittest.TestCase): | ||
232 | 23 | # The EntryResource uses the field values that can be written or might | ||
233 | 24 | # othwerise change as the basis for its ETags. The make_entry_etag_cores | ||
234 | 25 | # function is passed the data about the fields and returns the read and | ||
235 | 26 | # write cores. | ||
236 | 27 | |||
237 | 28 | def test_no_field_details(self): | ||
238 | 29 | # If make_entry_etag_cores is given no field details (because no | ||
239 | 30 | # fields exist), the resulting cores empty strings. | ||
240 | 31 | self.assertEquals(make_entry_etag_cores([]), ['', '']) | ||
241 | 32 | |||
242 | 33 | def test_writable_fields(self): | ||
243 | 34 | # If there are writable fields, their values are incorporated into the | ||
244 | 35 | # writable portion of the cores. | ||
245 | 36 | field_details = [ | ||
246 | 37 | ('first_field', | ||
247 | 38 | {'writable': True, | ||
248 | 39 | 'value': 'first'}), | ||
249 | 40 | ('second_field', | ||
250 | 41 | {'writable': True, | ||
251 | 42 | 'value': 'second'}), | ||
252 | 43 | ] | ||
253 | 44 | self.assertEquals( | ||
254 | 45 | make_entry_etag_cores(field_details), ['', 'first\0second']) | ||
255 | 46 | |||
256 | 47 | def test_unchanging_fields(self): | ||
257 | 48 | # If there are fields that are not writable their values are still | ||
258 | 49 | # reflected in the generated cores because we want and addition or | ||
259 | 50 | # removal of read-only fields to trigger a new ETag. | ||
260 | 51 | field_details = [ | ||
261 | 52 | ('first_field', | ||
262 | 53 | {'writable': False, | ||
263 | 54 | 'value': 'the value'}), | ||
264 | 55 | ] | ||
265 | 56 | self.assertEquals( | ||
266 | 57 | make_entry_etag_cores(field_details), | ||
267 | 58 | ['the value', '']) | ||
268 | 59 | |||
269 | 60 | def test_combinations_of_fields(self): | ||
270 | 61 | # If there are a combination of writable, changable, and unchanable | ||
271 | 62 | # fields, their values are reflected in the resulting cores. | ||
272 | 63 | field_details = [ | ||
273 | 64 | ('first_writable', | ||
274 | 65 | {'writable': True, | ||
275 | 66 | 'value': 'first-writable'}), | ||
276 | 67 | ('second_writable', | ||
277 | 68 | {'writable': True, | ||
278 | 69 | 'value': 'second-writable'}), | ||
279 | 70 | ('first_non_writable', | ||
280 | 71 | {'writable': False, | ||
281 | 72 | 'value': 'first-not-writable'}), | ||
282 | 73 | ('second_non_writable', | ||
283 | 74 | {'writable': False, | ||
284 | 75 | 'value': 'second-not-writable'}), | ||
285 | 76 | ] | ||
286 | 77 | self.assertEquals( | ||
287 | 78 | make_entry_etag_cores(field_details), | ||
288 | 79 | ['first-not-writable\x00second-not-writable', | ||
289 | 80 | 'first-writable\x00second-writable']) | ||
290 | 81 | |||
291 | 82 | |||
292 | 83 | class TestHTTPResourceETags(unittest.TestCase): | ||
293 | 84 | |||
294 | 85 | def test_getETag_is_a_noop(self): | ||
295 | 86 | # The HTTPResource class implements a do-nothing _getETagCores in order to | ||
296 | 87 | # be conservative (because it's not aware of the nature of all possible | ||
297 | 88 | # subclasses). | ||
298 | 89 | self.assertEquals(HTTPResource(None, None)._getETagCores(), None) | ||
299 | 90 | |||
300 | 91 | |||
301 | 92 | class TestHTTPResourceETags(unittest.TestCase): | ||
302 | 93 | |||
303 | 94 | def test_getETag_is_a_noop(self): | ||
304 | 95 | # The HTTPResource class implements a do-nothing _getETagCores in order to | ||
305 | 96 | # be conservative (because it's not aware of the nature of all possible | ||
306 | 97 | # subclasses). | ||
307 | 98 | self.assertEquals(HTTPResource(None, None)._getETagCores(), None) | ||
308 | 99 | |||
309 | 100 | |||
310 | 101 | class FauxEntryField: | ||
311 | 102 | entry = None | ||
312 | 103 | name = 'field_name' | ||
313 | 104 | field = None | ||
314 | 105 | |||
315 | 106 | |||
316 | 107 | class EntryFieldResourceTests(unittest.TestCase): | ||
317 | 108 | # Tests for ETags of EntryFieldResource objects. | ||
318 | 109 | |||
319 | 110 | # Because the ETag generation only takes into account the field value and | ||
320 | 111 | # the web service revision number (and not whether the field is read-write | ||
321 | 112 | # or read-only) these tests don't mention the read-write/read-only nature | ||
322 | 113 | # of the field in question. | ||
323 | 114 | |||
324 | 115 | def setUp(self): | ||
325 | 116 | self.config = TestWebServiceConfiguration() | ||
326 | 117 | provideUtility(self.config, IWebServiceConfiguration) | ||
327 | 118 | self.resource = EntryFieldResource(FauxEntryField(), None) | ||
328 | 119 | |||
329 | 120 | def set_field_value(self, value): | ||
330 | 121 | """Set the value of the fake field the EntryFieldResource references. | ||
331 | 122 | """ | ||
332 | 123 | self.resource._unmarshalled_field_cache['field_name'] = ( | ||
333 | 124 | 'field_name', value) | ||
334 | 125 | # We have to clear the etag cache for a new value to be generated. | ||
335 | 126 | # XXX benji 2010-09-30 [bug=652459] Does this mean there is an error | ||
336 | 127 | # condition that occurs when something other than applyChanges (which | ||
337 | 128 | # invalidates the cache) modifies a field's value? | ||
338 | 129 | self.resource.etags_by_media_type = {} | ||
339 | 130 | |||
340 | 131 | def test_cores_change_with_revno(self): | ||
341 | 132 | # The ETag cores should change if the revision (not the version) of | ||
342 | 133 | # the web service change. | ||
343 | 134 | self.set_field_value('this is the field value') | ||
344 | 135 | |||
345 | 136 | # Find the cores generated with a given revision... | ||
346 | 137 | self.config.code_revision = u'42' | ||
347 | 138 | first_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
348 | 139 | |||
349 | 140 | # ...find the cores generated with a different revision. | ||
350 | 141 | self.config.code_revision = u'99' | ||
351 | 142 | second_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
352 | 143 | |||
353 | 144 | # The cores should be different. | ||
354 | 145 | self.assertNotEqual(first_cores, second_cores) | ||
355 | 146 | # In particular, the read core should be the same between the two, but | ||
356 | 147 | # the write core should be different. | ||
357 | 148 | self.assertEqual(first_cores[1], second_cores[1]) | ||
358 | 149 | self.assertNotEqual(first_cores[0], second_cores[0]) | ||
359 | 150 | |||
360 | 151 | def test_cores_change_with_value(self): | ||
361 | 152 | # The ETag cores should change if the value of the field change. | ||
362 | 153 | # Find the cores generated with a given value... | ||
363 | 154 | self.set_field_value('first value') | ||
364 | 155 | first_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
365 | 156 | |||
366 | 157 | # ...find the cores generated with a different value. | ||
367 | 158 | self.set_field_value('second value') | ||
368 | 159 | second_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
369 | 160 | |||
370 | 161 | # The cores should be different. | ||
371 | 162 | self.assertNotEqual(first_cores, second_cores) | ||
372 | 163 | # In particular, the read core should be different between the two, | ||
373 | 164 | # but the write core should be the same. | ||
374 | 165 | self.assertNotEqual(first_cores[1], second_cores[1]) | ||
375 | 166 | self.assertEqual(first_cores[0], second_cores[0]) | ||
376 | 167 | |||
377 | 168 | |||
378 | 169 | class ServiceRootResourceTests(unittest.TestCase): | ||
379 | 170 | # Tests for ETags of EntryFieldResource objects. | ||
380 | 171 | |||
381 | 172 | def setUp(self): | ||
382 | 173 | self.config = TestWebServiceConfiguration() | ||
383 | 174 | provideUtility(self.config, IWebServiceConfiguration) | ||
384 | 175 | self.resource = ServiceRootResource() | ||
385 | 176 | |||
386 | 177 | def test_cores_change_with_revno(self): | ||
387 | 178 | # The ETag core should change if the revision (not the version) of the | ||
388 | 179 | # web service change. | ||
389 | 180 | |||
390 | 181 | # Find the cores generated with a given revision... | ||
391 | 182 | self.config.code_revision = u'42' | ||
392 | 183 | first_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
393 | 184 | |||
394 | 185 | # ...find the cores generated with a different revision. | ||
395 | 186 | self.config.code_revision = u'99' | ||
396 | 187 | second_cores = self.resource._getETagCores(self.resource.JSON_TYPE) | ||
397 | 188 | |||
398 | 189 | # The cores should be different. | ||
399 | 190 | self.assertNotEqual(first_cores, second_cores) | ||
400 | 191 | |||
401 | 192 | |||
402 | 193 | class TestableHTTPResource(HTTPResource): | ||
403 | 194 | """A HTTPResource that lest us set the ETags from the outside.""" | ||
404 | 195 | |||
405 | 196 | def _parseETags(self, *args): | ||
406 | 197 | return self.incoming_etags | ||
407 | 198 | |||
408 | 199 | def getETag(self, *args): | ||
409 | 200 | return self.existing_etag | ||
410 | 201 | |||
411 | 202 | |||
412 | 203 | class TestConditionalGet(unittest.TestCase): | ||
413 | 204 | |||
414 | 205 | def setUp(self): | ||
415 | 206 | self.config = TestWebServiceConfiguration() | ||
416 | 207 | provideUtility(self.config, IWebServiceConfiguration) | ||
417 | 208 | self.request = create_web_service_request('/1.0') | ||
418 | 209 | self.resource = TestableHTTPResource(None, self.request) | ||
419 | 210 | |||
420 | 211 | def test_etags_are_the_same(self): | ||
421 | 212 | # If one of the ETags present in an incoming request is the same as | ||
422 | 213 | # the ETag that represents the current object's state, then | ||
423 | 214 | # a conditional GET should return "Not Modified" (304). | ||
424 | 215 | self.resource.incoming_etags = ['1', '2', '3'] | ||
425 | 216 | self.resource.existing_etag = '2' | ||
426 | 217 | self.assertEquals(self.resource.handleConditionalGET(), None) | ||
427 | 218 | self.assertEquals(self.request.response.getStatus(), 304) | ||
428 | 219 | |||
429 | 220 | def test_etags_differ(self): | ||
430 | 221 | # If none of the ETags present in an incoming request is the same as | ||
431 | 222 | # the ETag that represents the current object's state, then a | ||
432 | 223 | # conditional GET should result in a new representation of the object | ||
433 | 224 | # being returned. | ||
434 | 225 | self.resource.incoming_etags = ['1', '2', '3'] | ||
435 | 226 | self.resource.existing_etag = '99' | ||
436 | 227 | self.assertNotEquals(self.resource.handleConditionalGET(), None) | ||
437 | 228 | |||
438 | 229 | |||
439 | 230 | class TestConditionalWrite(unittest.TestCase): | ||
440 | 231 | |||
441 | 232 | def setUp(self): | ||
442 | 233 | self.config = TestWebServiceConfiguration() | ||
443 | 234 | provideUtility(self.config, IWebServiceConfiguration) | ||
444 | 235 | self.request = create_web_service_request('/1.0') | ||
445 | 236 | self.resource = TestableHTTPResource(None, self.request) | ||
446 | 237 | |||
447 | 238 | def test_etags_are_the_same(self): | ||
448 | 239 | # If one of the ETags present in an incoming request is the same as | ||
449 | 240 | # the ETag that represents the current object's state, then | ||
450 | 241 | # the write should be applied. | ||
451 | 242 | self.resource.incoming_etags = ['1', '2', '3'] | ||
452 | 243 | self.resource.existing_etag = '2' | ||
453 | 244 | self.assertNotEquals(self.resource.handleConditionalWrite(), None) | ||
454 | 245 | |||
455 | 246 | def test_etags_differ(self): | ||
456 | 247 | # If one of the ETags present in an incoming request is the same as | ||
457 | 248 | # the ETag that represents the current object's state, then | ||
458 | 249 | # the write should fail. | ||
459 | 250 | self.resource.incoming_etags = ['1', '2', '3'] | ||
460 | 251 | self.resource.existing_etag = '99' | ||
461 | 252 | self.assertEquals(self.resource.handleConditionalWrite(), None) | ||
462 | 253 | self.assertEquals(self.request.response.getStatus(), 412) | ||
463 | 0 | 254 | ||
464 | === modified file 'src/lazr/restful/tests/test_utils.py' | |||
465 | --- src/lazr/restful/tests/test_utils.py 2010-08-24 22:12:38 +0000 | |||
466 | +++ src/lazr/restful/tests/test_utils.py 2010-10-04 15:46:40 +0000 | |||
467 | @@ -11,12 +11,25 @@ | |||
468 | 11 | from zope.security.management import ( | 11 | from zope.security.management import ( |
469 | 12 | endInteraction, newInteraction, queryInteraction) | 12 | endInteraction, newInteraction, queryInteraction) |
470 | 13 | 13 | ||
473 | 14 | from lazr.restful.utils import (get_current_browser_request, | 14 | from lazr.restful.utils import ( |
474 | 15 | is_total_size_link_active, sorted_named_things) | 15 | extract_write_portion, |
475 | 16 | get_current_browser_request, | ||
476 | 17 | is_total_size_link_active, | ||
477 | 18 | sorted_named_things, | ||
478 | 19 | ) | ||
479 | 16 | 20 | ||
480 | 17 | 21 | ||
481 | 18 | class TestUtils(unittest.TestCase): | 22 | class TestUtils(unittest.TestCase): |
482 | 19 | 23 | ||
483 | 24 | def test_two_part_extract_write_portion(self): | ||
484 | 25 | # ETags are sometimes two-part. A hyphen seperates the parts if so. | ||
485 | 26 | self.assertEqual('write', extract_write_portion('read-write')) | ||
486 | 27 | |||
487 | 28 | def test_one_part_extract_write_portion(self): | ||
488 | 29 | # ETags are sometimes one-part. If so, writes are predicated on the | ||
489 | 30 | # whole ETag. | ||
490 | 31 | self.assertEqual('etag', extract_write_portion('etag')) | ||
491 | 32 | |||
492 | 20 | def test_get_current_browser_request_no_interaction(self): | 33 | def test_get_current_browser_request_no_interaction(self): |
493 | 21 | # When there's no interaction setup, get_current_browser_request() | 34 | # When there's no interaction setup, get_current_browser_request() |
494 | 22 | # returns None. | 35 | # returns None. |
495 | @@ -77,4 +90,3 @@ | |||
496 | 77 | 90 | ||
497 | 78 | # For the sake of convenience, test_get_current_web_service_request() | 91 | # For the sake of convenience, test_get_current_web_service_request() |
498 | 79 | # and tag_request_with_version_name() are tested in test_webservice.py. | 92 | # and tag_request_with_version_name() are tested in test_webservice.py. |
499 | 80 | |||
500 | 81 | 93 | ||
501 | === modified file 'src/lazr/restful/utils.py' | |||
502 | --- src/lazr/restful/utils.py 2010-08-24 22:12:38 +0000 | |||
503 | +++ src/lazr/restful/utils.py 2010-10-04 15:46:40 +0000 | |||
504 | @@ -38,6 +38,11 @@ | |||
505 | 38 | missing = object() | 38 | missing = object() |
506 | 39 | 39 | ||
507 | 40 | 40 | ||
508 | 41 | def extract_write_portion(etag): | ||
509 | 42 | """Retrieve the portion of an etag that predicates writes.""" | ||
510 | 43 | return etag.split('-', 1)[-1] | ||
511 | 44 | |||
512 | 45 | |||
513 | 41 | def is_total_size_link_active(version, config): | 46 | def is_total_size_link_active(version, config): |
514 | 42 | versions = config.active_versions | 47 | versions = config.active_versions |
515 | 43 | total_size_link_version = config.first_version_with_total_size_link | 48 | total_size_link_version = config.first_version_with_total_size_link |
516 | 44 | 49 | ||
517 | === modified file 'versions.cfg' | |||
518 | --- versions.cfg 2010-08-24 20:04:30 +0000 | |||
519 | +++ versions.cfg 2010-10-04 15:46:40 +0000 | |||
520 | @@ -24,6 +24,7 @@ | |||
521 | 24 | lxml = 2.2.7 | 24 | lxml = 2.2.7 |
522 | 25 | martian = 0.11 | 25 | martian = 0.11 |
523 | 26 | pytz = 2010h | 26 | pytz = 2010h |
524 | 27 | roman = 1.4.0 | ||
525 | 27 | setuptools = 0.6c11 | 28 | setuptools = 0.6c11 |
526 | 28 | simplejson = 2.0.9 | 29 | simplejson = 2.0.9 |
527 | 29 | transaction = 1.0.0 | 30 | transaction = 1.0.0 |
I notice you are adding a roman numeral package to buildout, but are not using it.