Quellcode durchsuchen

Add exceptions conversion to MongoCollection

Olivier Lechevalier vor 10 Jahren
Ursprung
Commit
ad66fc03aa

+ 201 - 47
lib/Mongo/MongoCollection.php

@@ -15,6 +15,7 @@
 
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 
 /**
  * Represents a database collection.
@@ -151,7 +152,12 @@ class MongoCollection
 
         $command += $options;
 
-        return $this->db->command($command);
+        try {
+            return $this->db->command($command);
+        } catch (MongoCursorTimeoutException $e) {
+            throw new MongoExecutionTimeoutException($e->getMessage(), $e->getCode(), $e);
+        }
+
     }
 
     /**
@@ -258,14 +264,31 @@ class MongoCollection
     public function insert(&$a, array $options = [])
     {
         if (! $this->ensureDocumentHasMongoId($a)) {
-            trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_WARNING);
+            trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
             return;
         }
 
-        $result = $this->collection->insertOne(
-            TypeConverter::fromLegacy($a),
-            $this->convertWriteConcernOptions($options)
-        );
+        if (! count((array)$a)) {
+            throw new \MongoException('document must be an array or object');
+        }
+
+        try {
+            $result = $this->collection->insertOne(
+                TypeConverter::fromLegacy($a),
+                $this->convertWriteConcernOptions($options)
+            );
+        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
+            $writeResult = $e->getWriteResult();
+            $writeError = $writeResult->getWriteErrors()[0];
+            return [
+                'ok' => 0.0,
+                'n' => 0,
+                'err' => $writeError->getCode(),
+                'errmsg' => $writeError->getMessage(),
+            ];
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
         if (! $result->isAcknowledged()) {
             return true;
@@ -290,14 +313,37 @@ class MongoCollection
      */
     public function batchInsert(array &$a, array $options = [])
     {
+        if (empty($a)) {
+            throw new \MongoException('No write ops were included in the batch');
+        }
+
+        $continueOnError = isset($options['continueOnError']) && $options['continueOnError'];
+
         foreach ($a as $key => $item) {
-            $this->ensureDocumentHasMongoId($a[$key]);
+            try {
+                if (! $this->ensureDocumentHasMongoId($a[$key])) {
+                    if ($continueOnError) {
+                        unset($a[$key]);
+                    } else {
+                        trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
+                        return;
+                    }
+                }
+            } catch (MongoException $e) {
+                if ( ! $continueOnError) {
+                    throw $e;
+                }
+            }
         }
 
-        $result = $this->collection->insertMany(
-            TypeConverter::fromLegacy(array_values($a)),
-            $this->convertWriteConcernOptions($options)
-        );
+        try {
+            $result = $this->collection->insertMany(
+                TypeConverter::fromLegacy(array_values($a)),
+                $this->convertWriteConcernOptions($options)
+            );
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
         if (! $result->isAcknowledged()) {
             return true;
@@ -329,12 +375,27 @@ class MongoCollection
         $method = $multiple ? 'updateMany' : 'updateOne';
         unset($options['multiple']);
 
-        /** @var \MongoDB\UpdateResult $result */
-        $result = $this->collection->$method(
-            TypeConverter::fromLegacy($criteria),
-            TypeConverter::fromLegacy($newobj),
-            $this->convertWriteConcernOptions($options)
-        );
+        try {
+            /** @var \MongoDB\UpdateResult $result */
+            $result = $this->collection->$method(
+                TypeConverter::fromLegacy($criteria),
+                TypeConverter::fromLegacy($newobj),
+                $this->convertWriteConcernOptions($options)
+            );
+        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
+            $writeResult = $e->getWriteResult();
+            $writeError = $writeResult->getWriteErrors()[0];
+            return [
+                'ok' => 0.0,
+                'nModified' => $writeResult->getModifiedCount(),
+                'n' => $writeResult->getMatchedCount(),
+                'err' => $writeError->getCode(),
+                'errmsg' => $writeError->getMessage(),
+                'updatedExisting' => $writeResult->getUpsertedCount() == 0,
+            ];
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
         if (! $result->isAcknowledged()) {
             return true;
@@ -366,11 +427,15 @@ class MongoCollection
         $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
         $method = $multiple ? 'deleteMany' : 'deleteOne';
 
-        /** @var \MongoDB\DeleteResult $result */
-        $result = $this->collection->$method(
-            TypeConverter::fromLegacy($criteria),
-            $this->convertWriteConcernOptions($options)
-        );
+        try {
+            /** @var \MongoDB\DeleteResult $result */
+            $result = $this->collection->$method(
+                TypeConverter::fromLegacy($criteria),
+                $this->convertWriteConcernOptions($options)
+            );
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
         if (! $result->isAcknowledged()) {
             return true;
@@ -410,7 +475,11 @@ class MongoCollection
      */
     public function distinct($key, array $query = [])
     {
-        return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, $query));
+        try {
+            return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, $query));
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            return false;
+        }
     }
 
     /**
@@ -426,21 +495,26 @@ class MongoCollection
     public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
     {
         $query = TypeConverter::fromLegacy($query);
+        try {
+            if (isset($options['remove'])) {
+                unset($options['remove']);
+                $document = $this->collection->findOneAndDelete($query, $options);
+            } else {
+                $update = is_array($update) ? TypeConverter::fromLegacy($update) : [];
+
+                if (isset($options['new'])) {
+                    $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
+                    unset($options['new']);
+                }
 
-        if (isset($options['remove'])) {
-            unset($options['remove']);
-            $document = $this->collection->findOneAndDelete($query, $options);
-        } else {
-            $update = is_array($update) ? TypeConverter::fromLegacy($update) : [];
+                $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
 
-            if (isset($options['new'])) {
-                $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
-                unset($options['new']);
+                $document = $this->collection->findOneAndUpdate($query, $update, $options);
             }
-
-            $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
-
-            $document = $this->collection->findOneAndUpdate($query, $update, $options);
+        } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
+            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
         }
 
         if ($document) {
@@ -462,8 +536,15 @@ class MongoCollection
     public function findOne(array $query = [], array $fields = [], array $options = [])
     {
         $options = ['projection' => $fields] + $options;
+        try {
+            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            if (strpos($e->getMessage(), 'No suitable servers found') !== false) {
+                throw new MongoConnectionException($e->getMessage(), $e->getCode(), $e);
+            }
+            ExceptionConverter::toLegacy($e);
+        }
 
-        $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
         if ($document !== null) {
             $document = TypeConverter::toLegacy($document);
         }
@@ -481,17 +562,63 @@ class MongoCollection
      *
      * @todo This method does not yet return the correct result
      */
-    public function createIndex(array $keys, array $options = [])
+    public function createIndex($keys, array $options = [])
     {
-        // Note: this is what the result array should look like
-//        $expected = [
-//            'createdCollectionAutomatically' => true,
-//            'numIndexesBefore' => 1,
-//            'numIndexesAfter' => 2,
-//            'ok' => 1.0
-//        ];
+        if (is_string($keys)) {
+            if (empty($keys)) {
+                throw new MongoException('empty string passed as key field');
+            }
+            $keys = [$keys => 1];
+        }
+
+        if (is_object($keys)) {
+            $keys = (array) $keys;
+        }
+
+        if (! is_array($keys) || ! count($keys)) {
+            throw new MongoException('keys cannot be empty');
+        }
+
+        // duplicate
+        $neededOptions = ['unique' => 1, 'sparse' => 1, 'expireAfterSeconds' => 1, 'background' => 1, 'dropDups' => 1];
+        $indexOptions = array_intersect_key($options, $neededOptions);
+        $indexes = $this->collection->listIndexes();
+        foreach ($indexes as $index) {
+
+            if (! empty($options['name']) && $index->getName() === $options['name']) {
+                throw new \MongoResultException(sprintf('index with name: %s already exists', $index->getName()));
+            }
+
+            if ($index->getKey() == $keys) {
+                $currentIndexOptions = array_intersect_key($index->__debugInfo(), $neededOptions);
+
+                unset($currentIndexOptions['name']);
+                if ($currentIndexOptions != $indexOptions) {
+                    throw new \MongoResultException('Index with same keys but different options already exists');
+                }
 
-        return $this->collection->createIndex($keys, $options);
+                return [
+                    'createdCollectionAutomatically' => false,
+                    'numIndexesBefore' => count($indexes),
+                    'numIndexesAfter' => count($indexes),
+                    'note' => 'all indexes already exist',
+                    'ok' => 1.0
+                ];
+            }
+        }
+
+        try {
+            $this->collection->createIndex($keys, $this->convertWriteConcernOptions($options));
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
+
+        return [
+            'createdCollectionAutomatically' => true,
+            'numIndexesBefore' => count($indexes),
+            'numIndexesAfter' => count($indexes) + 1,
+            'ok' => 1.0
+        ];
     }
 
     /**
@@ -571,7 +698,11 @@ class MongoCollection
      */
     public function count($query = [], array $options = [])
     {
-        return $this->collection->count(TypeConverter::fromLegacy($query), $options);
+        try {
+            return $this->collection->count(TypeConverter::fromLegacy($query), $options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
     }
 
     /**
@@ -595,7 +726,12 @@ class MongoCollection
 
         $options['upsert'] = true;
 
-        return $this->update(['_id' => $id], ['$set' => $document], $options);
+        $result = $this->update(['_id' => $id], ['$set' => $a], $options);
+        if ($result['ok'] == 0.0) {
+            throw new \MongoCursorException();
+        }
+
+        return $result;
     }
 
     /**
@@ -757,17 +893,35 @@ class MongoCollection
      */
     private function ensureDocumentHasMongoId(&$document)
     {
+        $checkKeys = function($array) {
+            foreach (array_keys($array) as $key) {
+                if (is_int($key) || empty($key) || strpos($key, '*') === 1) {
+                    throw new \MongoException('document contain invalid key');
+                }
+            }
+        };
+
         if (is_array($document)) {
+            if (empty($document)) {
+                throw new \MongoException('document cannot be empty');
+            }
             if (! isset($document['_id'])) {
                 $document['_id'] = new \MongoId();
             }
 
+            $checkKeys($document);
+
             return $document['_id'];
         } elseif (is_object($document)) {
+            if (empty((array) $document)) {
+                throw new \MongoException('document cannot be empty');
+            }
             if (! isset($document->_id)) {
                 $document->_id = new \MongoId();
             }
 
+            $checkKeys((array) $document);
+
             return $document->_id;
         }
 

+ 352 - 6
tests/Alcaeus/MongoDbAdapter/MongoCollectionTest.php

@@ -41,12 +41,80 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('bar', 'foo', $object);
     }
 
+    public function testInsertInvalidData()
+    {
+        $this->setExpectedException('PHPUnit_Framework_Error_Warning', 'ongoCollection::insert expects parameter 1 to be an array or object, integer given');
+
+        $document = 8;
+        $this->getCollection()->insert($document);
+    }
+
+    public function testInsertEmptyArray()
+    {
+        $this->setExpectedException('MongoException', 'document cannot be empty');
+
+        $document = [];
+        $this->getCollection()->insert($document);
+    }
+
+    public function testInsertArrayWithNumericKeys()
+    {
+        $this->setExpectedException('MongoException', 'document contain invalid key');
+
+        $document = [1 => 'foo'];
+        $this->getCollection()->insert($document);
+    }
+
+    public function testInsertEmptyObject()
+    {
+        $this->setExpectedException('MongoException', 'document cannot be empty');
+
+        $document = (object) [];
+        $this->getCollection()->insert($document);
+    }
+
+    public function testInsertObjectWithPrivateProperties()
+    {
+        $this->setExpectedException('MongoException', 'document contain invalid key');
+
+        $document = $this->getCollection();
+        $this->getCollection()->insert($document);
+    }
+
+    public function testInsertDuplicate()
+    {
+        $collection = $this->getCollection();
+
+        $collection->createIndex(['foo' => 1], ['unique' => true]);
+
+        $document = ['foo' => 'bar'];
+        $collection->insert($document);
+
+        unset($document['_id']);
+        $this->assertArraySubset(
+            [
+                'ok' => 0.0,
+                'n' => 0,
+                'err' => 11000,
+            ],
+            $collection->insert($document)
+        );
+    }
+
     public function testUnacknowledgedWrite()
     {
         $document = ['foo' => 'bar'];
         $this->assertTrue($this->getCollection()->insert($document, ['w' => 0]));
     }
 
+    public function testInsertWriteConcernException()
+    {
+        $this->setExpectedException('MongoConnectionException');
+
+        $document = ['foo' => 'bar'];
+        $this->getCollection()->insert($document, ['w' => 2]);
+    }
+
     public function testInsertMany()
     {
         $expected = [
@@ -69,6 +137,7 @@ class MongoCollectionTest extends TestCase
         }
     }
 
+
     public function testInsertManyWithNonNumericKeys()
     {
         $expected = [
@@ -85,6 +154,53 @@ class MongoCollectionTest extends TestCase
             'b' => ['bar' => 'foo']
         ];
         $this->assertSame($expected, $this->getCollection()->batchInsert($documents));
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(2, $newCollection->count());
+    }
+
+    public function testBatchInsertContinuesOnError()
+    {
+        $expected = [
+            'connectionId' => 0,
+            'n' => 0,
+            'syncMillis' => 0,
+            'writtenTo' => null,
+            'err' => null,
+            'errmsg' => null
+        ];
+
+        $documents = [
+            8,
+            'b' => ['bar' => 'foo']
+        ];
+        $this->assertSame($expected, $this->getCollection()->batchInsert($documents, ['continueOnError' => true]));
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(1, $newCollection->count());
+    }
+
+    public function testBatchInsertException()
+    {
+        $this->setExpectedException('MongoConnectionException');
+
+        $documents = [['foo' => 'bar']];
+        $this->getCollection()->batchInsert($documents, ['w' => 2]);
+    }
+
+    public function testBatchInsertEmptyBatchException()
+    {
+        $this->setExpectedException('MongoException', 'No write ops were included in the batch');
+
+        $documents = [];
+        $this->getCollection()->batchInsert($documents, ['w' => 2]);
+    }
+
+    public function testUpdateWriteConcern()
+    {
+        $this->setExpectedException('MongoConnectionException'); // does not match driver
+
+        $this->getCollection()->update([], ['$set' => ['foo' => 'bar']], ['w' => 2]);
     }
 
     public function testUpdateOne()
@@ -111,6 +227,28 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(1, $this->getCheckDatabase()->selectCollection('test')->count(['foo' => 'foo']));
     }
 
+    public function testUpdateFail()
+    {
+        $collection = $this->getCollection();
+        $collection->createIndex(['foo' => 1], ['unique' => 1]);
+
+        $document = ['foo' => 'bar'];
+        $collection->insert($document);
+        $document = ['foo' => 'foo'];
+        $collection->insert($document);
+
+        $this->assertArraySubset(
+            [
+                'ok' => 0.0,
+                'nModified' => 0,
+                'n' => 0,
+                'err' => 11000,
+                'updatedExisting' => true,
+            ],
+            $collection->update(['foo' => 'bar'], ['$set' => ['foo' => 'foo']])
+        );
+    }
+
     public function testUpdateMany()
     {
         $document = ['change' => true, 'foo' => 'bar'];
@@ -220,6 +358,15 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(2, $collection->count(['foo' => 'bar']));
     }
 
+    public function testCountTimeout()
+    {
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $this->getCollection()->count([], ['maxTimeMS' => 1]);
+    }
+
     public function testFindOne()
     {
         $this->prepareData();
@@ -228,6 +375,16 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(['foo' => 'foo'], $document);
     }
 
+    public function testFindOneConnectionIssue()
+    {
+        $client = $this->getClient([], 'mongodb://localhost:28888?connectTimeoutMS=1');
+        $collection = $client->selectCollection('mongo-php-adapter', 'test');
+
+        $this->setExpectedException('MongoConnectionException');
+
+        $collection->findOne();
+    }
+
     public function testDistinct()
     {
         $this->prepareData();
@@ -276,6 +433,29 @@ class MongoCollectionTest extends TestCase
         ], $result['result']);
     }
 
+    public function testAggregateTimeoutException()
+    {
+        $collection = $this->getCollection();
+
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $pipeline = [
+            [
+                '$group' => [
+                    '_id' => '$foo',
+                    'count' => [ '$sum' => 1 ],
+                ],
+            ],
+            [
+                '$sort' => ['_id' => 1]
+            ]
+        ];
+
+        $collection->aggregate($pipeline, ['maxTimeMS' => 1]);
+    }
+
     public function testAggregateCursor()
     {
         $collection = $this->getCollection();
@@ -369,12 +549,20 @@ class MongoCollectionTest extends TestCase
 
     public function testSaveInsert()
     {
+        $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
 
-        $document = ['foo' => 'bar'];
-        $collection->save($document);
-        $this->assertInstanceOf('MongoId', $document['_id']);
-        $id = (string) $document['_id'];
+        $expected = [
+            'ok' => 1.0,
+            'nModified' => 0,
+            'n' => 0,
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => false,
+        ];
+
+        $document = ['_id' => new \MongoId($id), 'foo' => 'bar'];
+        $this->assertSame($expected, $collection->save($document));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
@@ -402,13 +590,22 @@ class MongoCollectionTest extends TestCase
 
     public function testSaveUpdate()
     {
+        $expected = [
+            'ok' => 1.0,
+            'nModified' => 1,
+            'n' => 1,
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => true,
+        ];
+
         $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
 
         $insertDocument = ['_id' => new \MongoId($id), 'foo' => 'bar'];
         $saveDocument = ['_id' => new \MongoId($id), 'foo' => 'foo'];
         $collection->insert($insertDocument);
-        $collection->save($saveDocument);
+        $this->assertSame($expected, $collection->save($saveDocument));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
@@ -421,6 +618,54 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('foo', 'foo', $object);
     }
 
+    public function testSaveDuplicate()
+    {
+        $collection = $this->getCollection();
+
+        $collection->createIndex(['foo' => 1], ['unique' => true]);
+
+        $document = ['foo' => 'bar'];
+        $collection->save($document);
+
+        $this->setExpectedException('MongoCursorException');
+
+        unset($document['_id']);
+        $this->assertArraySubset(
+            [
+                'ok' => 0.0,
+                'nModified' => 0,
+                'n' => 0,
+                'err' => 11000,
+                'updatedExisting' => true,
+            ],
+            $collection->save($document)
+        );
+    }
+
+    public function testSaveEmptyKeys()
+    {
+        $this->setExpectedException('MongoException');
+
+        $document = [];
+        $this->getCollection()->save($document);
+    }
+
+    public function testSaveEmptyObject()
+    {
+        $this->setExpectedException('MongoException');
+
+        $document = (object) [];
+        $this->getCollection()->save($document);
+    }
+
+    public function testSaveWrite()
+    {
+        $this->setExpectedException('MongoConnectionException'); // should be MongoCursorException
+
+        $document = ['foo' => 'bar'];
+        $this->getCollection()->save($document, ['w' => 2, 'wtimeout' => 1000]);
+    }
+
     public function testGetDBRef()
     {
         $collection = $this->getCollection();
@@ -450,8 +695,15 @@ class MongoCollectionTest extends TestCase
 
     public function testCreateIndex()
     {
+        $expected = [
+            'createdCollectionAutomatically' => true,
+            'numIndexesBefore' => 1,
+            'numIndexesAfter' => 2,
+            'ok' => 1.0,
+        ];
+
         $collection = $this->getCollection();
-        $collection->createIndex(['foo' => 1]);
+        $this->assertSame($expected, $collection->createIndex(['foo' => 1]));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $iterator = $newCollection->listIndexes();
@@ -462,6 +714,45 @@ class MongoCollectionTest extends TestCase
         $this->assertSame('mongo-php-adapter.test', $index->getNamespace());
     }
 
+    public function testCreateIndexInvalid()
+    {
+        $this->setExpectedException('MongoException', 'keys cannot be empty');
+
+        $this->getCollection()->createIndex([]);
+    }
+
+    public function testCreateIndexTwice()
+    {
+        $this->getCollection()->createIndex(['foo' => 1]);
+
+        $expected = [
+            'createdCollectionAutomatically' => false,
+            'numIndexesBefore' => 1,
+            'numIndexesAfter' => 1,
+            'note' => 'all indexes already exist',
+            'ok' => 1.0
+        ];
+        $this->assertSame($expected, $this->getCollection()->createIndex(['foo' => 1]));
+    }
+
+    public function testCreateIndexesWithDifferentOptions()
+    {
+        $this->setExpectedException('MongoResultException');
+
+        $this->getCollection()->createIndex(['foo' => 1]);
+
+        $this->getCollection()->createIndex(['foo' => 1], ['unique' => true]);
+    }
+
+    public function testCreateIndexWithSameName()
+    {
+        $this->setExpectedException('MongoResultException');
+
+        $this->getCollection()->createIndex(['foo' => 1], ['name' => 'foo']);
+
+        $this->getCollection()->createIndex(['bar' => 1], ['name' => 'foo']);
+    }
+
     public function testEnsureIndex()
     {
         $collection = $this->getCollection();
@@ -630,6 +921,38 @@ class MongoCollectionTest extends TestCase
         );
     }
 
+    public function testFindAndModifyResultException()
+    {
+        $id = '54203e08d51d4a1f868b456e';
+        $collection = $this->getCollection();
+
+        $this->setExpectedException('MongoResultException');
+
+        $document = $collection->findAndModify(
+            array("inprogress" => false, "name" => "Next promo"),
+            array('$pop' => array("tasks" => -1)),
+            array("tasks" => array('$pop' => array("stuff"))),
+            array("new" => true)
+        );
+    }
+
+    public function testFindAndModifyExceptionTimeout()
+    {
+        $this->failMaxTimeMS();
+
+        $id = '54203e08d51d4a1f868b456e';
+        $collection = $this->getCollection();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $document = $collection->findAndModify(
+            ['_id' => new \MongoId($id)],
+            null,
+            null,
+            ['maxTimeMS' => 1, 'remove' => true]
+        );
+    }
+
     public function testFindAndModifyRemove()
     {
         $id = '54203e08d51d4a1f868b456e';
@@ -683,4 +1006,27 @@ class MongoCollectionTest extends TestCase
         ];
         $this->assertSame($expected, $this->getCollection()->drop());
     }
+
+    public function testEmptyCollectionName()
+    {
+        $this->setExpectedException('Exception', 'Collection name cannot be empty');
+
+        new \MongoCollection($this->getDatabase(), '');
+    }
+
+    public function testSelectCollectionWithNullBytes()
+    {
+        $this->setExpectedException('Exception', 'Collection name cannot contain null bytes');
+
+        new \MongoCollection($this->getDatabase(), 'foo' . chr(0));
+    }
+
+    public function testSubCollectionWithNullBytes()
+    {
+        $collection = $this->getCollection();
+
+        $this->setExpectedException('Exception', 'Collection name cannot contain null bytes');
+
+        $collection->{'foo' . chr(0)};
+    }
 }

+ 2 - 2
tests/Alcaeus/MongoDbAdapter/TestCase.php

@@ -32,9 +32,9 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
      * @param array|null $options
      * @return \MongoClient
      */
-    protected function getClient($options = null)
+    protected function getClient($options = null, $uri = 'mongodb://localhost')
     {
-        $args = ['mongodb://localhost'];
+        $args = [$uri];
         if ($options !== null) {
             $args[] = $options;
         }