Repository: Kinto/kinto

Issue:
Crash in history with DELETE /buckets
```
ERROR  "DELETE /v1/buckets" ? (? ms) 'bucket_id' agent=node-fetch/1.0 (+https://github.com/bitinn/node-fetch) authn_type=basicauth collection_id=bucket collection_timestamp=1471536086687 errno=None exception=Traceback (most recent call last):
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 101, in tm_tween
    reraise(*exc_info)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 90, in tm_tween
    manager.commit()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_manager.py", line 111, in commit
    return self.get().commit()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_transaction.py", line 265, in commit
    self._callBeforeCommitHooks()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_transaction.py", line 337, in _callBeforeCommitHooks
    hook(*args, **kws)
  File "/home/mathieu/Code/Mozilla/kinto/kinto/core/events.py", line 76, in _notify_resource_events_before
    request.registry.notify(event)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/registry.py", line 91, in notify
    [ _ for _ in self.subscribers(events, None) ]
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/zope/interface/registry.py", line 328, in subscribers
    return self.adapters.subscribers(objects, provided)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/zope/interface/adapter.py", line 596, in subscribers
    subscription(*objects)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/config/adapters.py", line 130, in subscriber_wrapper
    return derived_subscriber(*arg)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/config/adapters.py", line 103, in derived_subscriber
    return subscriber(arg[0])
  File "/home/mathieu/Code/Mozilla/kinto/kinto/plugins/history/listener.py", line 16, in on_resource_changed
    bucket_id = payload.pop('bucket_id')
KeyError: 'bucket_id' lang=None uid=a9d81e1b56f2b77103e87601621a99b3d82a15abf7c72ce57a032d0964317c7a
```

Crash in history with DELETE /buckets
```
ERROR  "DELETE /v1/buckets" ? (? ms) 'bucket_id' agent=node-fetch/1.0 (+https://github.com/bitinn/node-fetch) authn_type=basicauth collection_id=bucket collection_timestamp=1471536086687 errno=None exception=Traceback (most recent call last):
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 101, in tm_tween
    reraise(*exc_info)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 90, in tm_tween
    manager.commit()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_manager.py", line 111, in commit
    return self.get().commit()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_transaction.py", line 265, in commit
    self._callBeforeCommitHooks()
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/transaction/_transaction.py", line 337, in _callBeforeCommitHooks
    hook(*args, **kws)
  File "/home/mathieu/Code/Mozilla/kinto/kinto/core/events.py", line 76, in _notify_resource_events_before
    request.registry.notify(event)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/registry.py", line 91, in notify
    [ _ for _ in self.subscribers(events, None) ]
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/zope/interface/registry.py", line 328, in subscribers
    return self.adapters.subscribers(objects, provided)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/zope/interface/adapter.py", line 596, in subscribers
    subscription(*objects)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/config/adapters.py", line 130, in subscriber_wrapper
    return derived_subscriber(*arg)
  File "/home/mathieu/Code/Mozilla/kinto-client/.venv/local/lib/python2.7/site-packages/pyramid/config/adapters.py", line 103, in derived_subscriber
    return subscriber(arg[0])
  File "/home/mathieu/Code/Mozilla/kinto/kinto/plugins/history/listener.py", line 16, in on_resource_changed
    bucket_id = payload.pop('bucket_id')
KeyError: 'bucket_id' lang=None uid=a9d81e1b56f2b77103e87601621a99b3d82a15abf7c72ce57a032d0964317c7a
```



Discussion:
`bucket_id = payload.pop('bucket_id', None)`

`bucket_id = payload.pop('bucket_id', None)`


Resolving patch:
diff --git a/kinto/plugins/history/listener.py b/kinto/plugins/history/listener.py
--- a/kinto/plugins/history/listener.py
+++ b/kinto/plugins/history/listener.py
@@ -1,5 +1,4 @@
 from kinto.core.utils import instance_uri
-import copy
 from datetime import datetime
 
 
@@ -9,30 +8,38 @@ def on_resource_changed(event):
     ``history`` resource. The entries are served as read-only in the
     :mod:`kinto.plugins.history.views` module.
     """
-    payload = copy.deepcopy(event.payload)
+    payload = event.payload
     resource_name = payload['resource_name']
     event_uri = payload['uri']
 
-    bucket_id = payload.pop('bucket_id')
-    bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)
-    collection_id = None
+    bucket_id = None
+    bucket_uri = None
     collection_uri = None
-    if 'collection_id' in payload:
-        collection_id = payload['collection_id']
-        collection_uri = instance_uri(event.request,
-                                      'collection',
-                                      bucket_id=bucket_id,
-                                      id=collection_id)
 
     storage = event.request.registry.storage
     permission = event.request.registry.permission
 
     targets = []
     for impacted in event.impacted_records:
-        # On POST .../records, the URI does not contain the newly created
-        # record id.
         target = impacted['new']
         obj_id = target['id']
+
+        try:
+            bucket_id = payload['bucket_id']
+        except KeyError:
+            # e.g. DELETE /buckets
+            bucket_id = obj_id
+        bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)
+
+        if 'collection_id' in payload:
+            collection_id = payload['collection_id']
+            collection_uri = instance_uri(event.request,
+                                          'collection',
+                                          bucket_id=bucket_id,
+                                          id=collection_id)
+
+        # On POST .../records, the URI does not contain the newly created
+        # record id.
         parts = event_uri.split('/')
         if resource_name in parts[-1]:
             parts.append(obj_id)
@@ -70,6 +77,7 @@ def on_resource_changed(event):
         # Prepare the history entry attributes.
         perms = {k: list(v) for k, v in perms_by_object_id[uri].items()}
         eventattrs = dict(**payload)
+        eventattrs.pop('bucket_id', None)
         eventattrs.setdefault('%s_id' % resource_name, obj_id)
         eventattrs['uri'] = uri
         attrs = dict(date=datetime.now().isoformat(),


Test patch:
diff --git a/tests/plugins/test_history.py b/tests/plugins/test_history.py
--- a/tests/plugins/test_history.py
+++ b/tests/plugins/test_history.py
@@ -79,6 +79,16 @@ def test_history_contains_bucket_creation(self):
         assert entry['action'] == 'create'
         assert entry['uri'] == '/buckets/test'
 
+    def test_history_supports_creation_via_plural_endpoint(self):
+        resp = self.app.post_json('/buckets', {'data': {'id': 'posted'}},
+                                  headers=self.headers)
+        resp = self.app.get('/buckets/posted/history', headers=self.headers)
+        entry = resp.json['data'][0]
+        assert entry['resource_name'] == 'bucket'
+        assert entry['bucket_id'] == 'posted'
+        assert entry['action'] == 'create'
+        assert entry['uri'] == '/buckets/posted'
+
     def test_tracks_bucket_attributes_update(self):
         body = {'data': {'foo': 'baz'}}
         self.app.patch_json(self.bucket_uri, body,
@@ -104,6 +114,24 @@ def test_bucket_delete_destroys_its_history_entries(self):
                                                collection_id='history')
         assert len(stored_in_backend) == 0
 
+    def test_delete_all_buckets_destroys_history_entries(self):
+        self.app.put_json('/buckets/1', {"data": {"a": 1}},
+                          headers=self.headers)
+
+        self.app.delete('/buckets?a=1', headers=self.headers)
+
+        # Entries about deleted bucket are gone.
+        storage = self.app.app.registry.storage
+        stored_in_backend, _ = storage.get_all(parent_id='/buckets/1',
+                                               collection_id='history')
+        assert len(stored_in_backend) == 0
+
+        # Entries of other buckets are still here.
+        resp = self.app.get(self.history_uri, headers=self.headers)
+        entry = resp.json['data'][-1]
+        assert entry['bucket_id'] == 'test'
+        assert entry['action'] == 'create'
+
     #
     # Collection
     #