Repository: Kinto/kinto

Issue:
Etag doesn't show on post and delete operations
According to the docs:

> The ETag header with the current timestamp of the list for the current user will be given on list endpoint.
On object enpoints, the ETag header value will contain the timestamp of the object.

But ETag is not present on POST and DELETE operations. Is this expected? If it is, may we document it?

```
$ http post localhost:8888/v1/buckets/b1/collections/c1/records -a aaa: 
HTTP/1.1 201 Created
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 187
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:19:31 GMT
Server: waitress
(...)

$ http delete localhost:8888/v1/buckets/b1/collections/c1/records -a aaa: 
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 101
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:19:46 GMT
Server: waitress
(...)

$ http delete localhost:8888/v1/buckets/b1/collections/c1 -a aaa: 
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 65
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:20:02 GMT
Server: waitress
...
```
Etag doesn't show on post and delete operations
According to the docs:

> The ETag header with the current timestamp of the list for the current user will be given on list endpoint.
On object enpoints, the ETag header value will contain the timestamp of the object.

But ETag is not present on POST and DELETE operations. Is this expected? If it is, may we document it?

```
$ http post localhost:8888/v1/buckets/b1/collections/c1/records -a aaa: 
HTTP/1.1 201 Created
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 187
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:19:31 GMT
Server: waitress
(...)

$ http delete localhost:8888/v1/buckets/b1/collections/c1/records -a aaa: 
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 101
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:19:46 GMT
Server: waitress
(...)

$ http delete localhost:8888/v1/buckets/b1/collections/c1 -a aaa: 
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff
Content-Length: 65
Content-Type: application/json; charset=UTF-8
Date: Mon, 12 Dec 2016 18:20:02 GMT
Server: waitress
...
```


Discussion:
We should probably add it.
The same happens for `Last-Modified`.
We should probably add it.
The same happens for `Last-Modified`.

Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -342,6 +342,8 @@ def collection_post(self):
             record = self.model.create_record(new_record)
             self.request.response.status_code = 201
             action = ACTIONS.CREATE
+        timestamp = record[self.model.modified_field]
+        self._add_timestamp_header(self.request.response, timestamp=timestamp)
         return self.postprocess(record, action=action)
 
     def collection_delete(self):
@@ -361,6 +363,13 @@ def collection_delete(self):
         records, _ = self.model.get_records(filters=filters)
         deleted = self.model.delete_records(filters=filters)
 
+        if deleted:
+            # Get timestamp of the last deleted field
+            timestamp = deleted[-1][self.model.modified_field]
+            self._add_timestamp_header(self.request.response, timestamp=timestamp)
+        else:
+            self._add_timestamp_header(self.request.response)
+
         action = len(deleted) > 0 and ACTIONS.DELETE or ACTIONS.READ
         return self.postprocess(deleted, action=action, old=records)
 
@@ -565,6 +574,9 @@ def delete(self):
                 last_modified = None
 
         deleted = self.model.delete_record(record, last_modified=last_modified)
+        timestamp = deleted[self.model.modified_field]
+        self._add_timestamp_header(self.request.response, timestamp=timestamp)
+
         return self.postprocess(deleted, action=ACTIONS.DELETE, old=record)
 
     #


Test patch:
diff --git a/tests/core/resource/test_record.py b/tests/core/resource/test_record.py
--- a/tests/core/resource/test_record.py
+++ b/tests/core/resource/test_record.py
@@ -135,6 +135,19 @@ def test_delete_record_returns_last_timestamp(self):
         result = self.resource.delete()['data']
         self.assertNotEqual(result['last_modified'], record['last_modified'])
 
+    def test_etag_is_provided(self):
+        record = self.model.create_record({'field': 'value'})
+        self.resource.record_id = record['id']
+        self.resource.delete()
+        self.assertIn('ETag', self.last_response.headers)
+
+    def test_etag_contains_deleted_timestamp(self):
+        record = self.model.create_record({'field': 'value'})
+        self.resource.record_id = record['id']
+        deleted = self.resource.delete()
+        expected = ('"%s"' % deleted['data']['last_modified'])
+        self.assertEqual(expected, self.last_response.headers['ETag'])
+
     def test_delete_record_returns_stripped_record(self):
         record = self.model.create_record({'field': 'value'})
         self.resource.record_id = record['id']
diff --git a/tests/core/resource/test_sync.py b/tests/core/resource/test_sync.py
--- a/tests/core/resource/test_sync.py
+++ b/tests/core/resource/test_sync.py
@@ -54,7 +54,7 @@ def test_filter_with__to_return_an_alert_header(self):
                         '.html#list-of-available-url-parameters')
             })
 
-    def test_the_timestamp_header_is_equal_to_last_modification(self):
+    def test_get_timestamp_header_is_equal_to_last_modification(self):
         result = self.resource.collection_post()['data']
         modification = result['last_modified']
         self.resource = self.resource_class(request=self.get_request(),
@@ -63,6 +63,21 @@ def test_the_timestamp_header_is_equal_to_last_modification(self):
         header = int(self.last_response.headers['ETag'][1:-1])
         self.assertEqual(header, modification)
 
+    def test_delete_timestamp_header_is_equal_to_last_deleted(self):
+        self.resource.collection_post()['data']
+        self.resource = self.resource_class(request=self.get_request(),
+                                            context=self.get_context())
+        result = self.resource.collection_delete()
+        modification = max([record['last_modified'] for record in result['data']])
+        header = int(self.last_response.headers['ETag'][1:-1])
+        self.assertEqual(header, modification)
+
+    def test_post_timestamp_header_is_equal_to_creation(self):
+        result = self.resource.collection_post()['data']
+        modification = result['last_modified']
+        header = int(self.last_response.headers['ETag'][1:-1])
+        self.assertEqual(header, modification)
+
     def test_filter_with_since_accepts_numeric_value(self):
         self.resource.request.GET = {'_since': '6'}
         self.resource.collection_post()