Repository: Kinto/kinto

Issue:
Create Collection event notification not fired for auto-create on default bucket
I was hacking around with Redis event notifications today. I setup two separate listeners with filters:

```
kinto.event_listeners = redis1 redis2

kinto.event_listeners.redis1.use = cliquet.listeners.redis
kinto.event_listeners.redis1.url = <url>
kinto.event_listeners.redis1.pool_size = 5
kinto.event_listeners.redis1.listname = kinto:collection:create
kinto.event_listeners.redis1.actions = create
kinto.event_listeners.redis1.resources = collection

kinto.event_listeners.redis2.use = cliquet.listeners.redis
kinto.event_listeners.redis2.url = <url>
kinto.event_listeners.redis2.pool_size = 5
kinto.event_listeners.redis2.listname = kinto:record:create
kinto.event_listeners.redis2.actions = create
kinto.event_listeners.redis2.resources = record
```

To test, I was using the default bucket, and the inserting a sample record to a collection that did not yet exist. Although the collection was auto-created, and the record inserted, only the event for create record was logged to Redis. If I create the collection manually, then the create collection event is logged.

Summary: Auto-created collections on the default bucket are not firing event notifications.



Discussion:
This is because the default bucket plugin [doesn't call](https://github.com/Kinto/kinto/blob/master/kinto/plugins/default_bucket/__init__.py#L77) the [postprocess method](https://github.com/mozilla-services/cliquet/blob/master/cliquet/resource/__init__.py#L625).

Oh, very nice catch!


Resolving patch:
diff --git a/kinto/plugins/default_bucket/__init__.py b/kinto/plugins/default_bucket/__init__.py
--- a/kinto/plugins/default_bucket/__init__.py
+++ b/kinto/plugins/default_bucket/__init__.py
@@ -6,6 +6,7 @@
 from pyramid.security import NO_PERMISSION_REQUIRED, Authenticated
 
 from cliquet.errors import raise_invalid
+from cliquet.events import ACTIONS
 from cliquet.utils import build_request, reapply_cors, hmac_digest
 from cliquet.storage import exceptions as storage_exceptions
 
@@ -27,14 +28,11 @@ def create_bucket(request, bucket_id):
     if bucket_id in already_created:
         return
 
-    # Fake context to instantiate a Bucket resource.
-    context = RouteFactory(request)
-    context.get_permission_object_id = lambda r, i: '/buckets/%s' % bucket_id
-    resource = Bucket(request, context)
-    try:
-        bucket = resource.model.create_record({'id': bucket_id})
-    except storage_exceptions.UnicityError as e:
-        bucket = e.record
+    bucket = resource_create_object(request=request,
+                                    resource_cls=Bucket,
+                                    uri='/buckets/%s' % bucket_id,
+                                    resource_name='bucket',
+                                    obj_id=bucket_id)
     already_created[bucket_id] = bucket
 
 
@@ -58,27 +56,51 @@ def create_collection(request, bucket_id):
     if collection_put:
         return
 
-    # Fake context to instantiate a Collection resource.
-    context = RouteFactory(request)
-    context.get_permission_object_id = lambda r, i: collection_uri
-
-    backup = request.matchdict
+    backup_matchdict = request.matchdict
     request.matchdict = dict(bucket_id=bucket_id,
                              id=collection_id,
                              **request.matchdict)
-    resource = Collection(request, context)
-    if not resource.model.id_generator.match(collection_id):
+    collection = resource_create_object(request=request,
+                                        resource_cls=Collection,
+                                        uri=collection_uri,
+                                        resource_name='collection',
+                                        obj_id=collection_id)
+    already_created[collection_uri] = collection
+    request.matchdict = backup_matchdict
+
+
+def resource_create_object(request, resource_cls, uri, resource_name, obj_id):
+    """In the default bucket, the bucket and collection are implicitly
+    created. This helper instantiate the resource and simulate a request
+    with its RootFactory on the instantiated resource.
+    :returns: the created object
+    :rtype: dict
+    """
+    # Fake context to instantiate a resource.
+    context = RouteFactory(request)
+    context.get_permission_object_id = lambda r, i: uri
+
+    resource = resource_cls(request, context)
+
+    # Check that provided id is valid for this resource.
+    if not resource.model.id_generator.match(obj_id):
         error_details = {
             'location': 'path',
-            'description': "Invalid collection_id id"
+            'description': "Invalid %s id" % resource_name
         }
-        raise_invalid(request, **error_details)
+        raise_invalid(resource.request, **error_details)
+
+    data = {'id': obj_id}
     try:
-        collection = resource.model.create_record({'id': collection_id})
+        obj = resource.model.create_record(data)
+        # Since the current request is not a resource (but a straight Service),
+        # we simulate a request on a resource.
+        # This will be used in the resource event payload.
+        resource.request.current_resource_name = resource_name
+        resource.postprocess(data, action=ACTIONS.CREATE)
     except storage_exceptions.UnicityError as e:
-        collection = e.record
-    already_created[collection_uri] = collection
-    request.matchdict = backup
+        obj = e.record
+    return obj
 
 
 def default_bucket(request):


Test patch:
diff --git a/kinto/plugins/default_bucket/test_plugin.py b/kinto/plugins/default_bucket/test_plugin.py
--- a/kinto/plugins/default_bucket/test_plugin.py
+++ b/kinto/plugins/default_bucket/test_plugin.py
@@ -220,6 +220,59 @@ def test_flush_capability_if_enabled(self):
         self.assertIn('default_bucket', capabilities)
 
 
+_events = []
+
+
+def load_from_config(config, prefix):
+    def listener(event):
+        _events.append(event)
+
+    return listener
+
+
+class EventsTest(BaseWebTest, unittest.TestCase):
+    def tearDown(self):
+        super(EventsTest, self).tearDown()
+        del _events[:]
+
+    def get_app_settings(self, extra=None):
+        settings = super(EventsTest, self).get_app_settings(extra)
+        settings['event_listeners'] = 'testevent',
+        settings['event_listeners.testevent.use'] = (
+            'kinto.plugins.default_bucket.test_plugin')
+        return settings
+
+    def test_an_event_is_sent_on_implicit_bucket_creation(self):
+        bucket_url = '/buckets/default'
+        self.app.get(bucket_url, headers=self.headers)
+        assert len(_events) == 1
+        payload = _events[-1].payload
+        assert payload['resource_name'] == 'bucket'
+        assert payload['action'] == 'create'
+
+    def test_an_event_is_sent_on_implicit_collection_creation(self):
+        collection_url = '/buckets/default/collections/articles'
+        self.app.get(collection_url, headers=self.headers)
+        assert len(_events) == 2
+        payload = _events[-1].payload
+        assert payload['resource_name'] == 'collection'
+        assert payload['action'] == 'create'
+
+    def test_events_sent_on_bucket_and_collection_creation(self):
+        records_uri = '/buckets/default/collections/articles/records'
+        self.app.get(records_uri, headers=self.headers)
+
+        assert len(_events) == 2
+
+        # XXX: Could not achieve a behaviour the payload uri reflect the
+        # underlying created object.
+        # resp = self.app.get('/', headers=self.headers)
+        # bucket_id = resp.json['user']['bucket']
+        # assert _events[0].payload['uri'] == '/buckets/%s' % bucket_id
+        # assert _events[1].payload['uri'] == (
+        #     '/buckets/%s/collections/articles' % bucket_id)
+
+
 class ReadonlyDefaultBucket(BaseWebTest, unittest.TestCase):
 
     def get_app_settings(self, extras=None):