소스 검색

Merge branch 'exceptions-converter'

Closes #20.

* exceptions-converter:
  Fix failing test
  Add exceptions to GridFS
  Corrected issue with MongoGridFSCursor::key method
  Add exceptions to MongoCursor
  Add exception handling to MongoClient
  Add exceptions conversion to MongoCollection
  Add exceptions handling to MongoDB.
  Implement ExceptionConverter
Andreas Braun 10 년 전
부모
커밋
ee831b8ccb

+ 65 - 0
lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php

@@ -0,0 +1,65 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+namespace Alcaeus\MongoDbAdapter;
+
+use MongoDB\Driver\Exception;
+
+/**
+ * @internal
+ */
+class ExceptionConverter
+{
+    /**
+     * @return \MongoException
+     */
+    public static function convertException(Exception\Exception $e)
+    {
+        switch (get_class($e)) {
+            case Exception\AuthenticationException::class:
+            case Exception\ConnectionException::class:
+            case Exception\ConnectionTimeoutException::class:
+            case Exception\SSLConnectionException::class:
+                $class = 'MongoConnectionException';
+                break;
+
+            case Exception\BulkWriteException::class:
+            case Exception\WriteException::class:
+                $class = 'MongoCursorException';
+                break;
+
+            case Exception\ExecutionTimeoutException::class:
+                $class = 'MongoExecutionTimeoutException';
+                break;
+
+            default:
+                $class = 'MongoException';
+        }
+
+        if (strpos($e->getMessage(), 'No suitable servers found') !== false) {
+            return new \MongoConnectionException($e->getMessage(), $e->getCode(), $e);
+        }
+
+        return new $class($e->getMessage(), $e->getCode(), $e);
+    }
+
+    /**
+     * @throws \MongoException
+     */
+    public static function toLegacy(Exception\Exception $e)
+    {
+        throw self::convertException($e);
+    }
+}

+ 18 - 5
lib/Mongo/MongoClient.php

@@ -14,6 +14,7 @@
  */
  */
 
 
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\Helper;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 use MongoDB\Client;
 use MongoDB\Client;
 
 
 /**
 /**
@@ -91,6 +92,7 @@ class MongoClient
         }
         }
     }
     }
 
 
+
     /**
     /**
      * Closes this database connection
      * Closes this database connection
      *
      *
@@ -181,8 +183,15 @@ class MongoClient
     {
     {
         $this->forceConnect();
         $this->forceConnect();
 
 
-        $servers = [];
-        foreach ($this->manager->getServers() as $server) {
+        $results = [];
+
+        try {
+            $servers = $this->manager->getServers();
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
+
+        foreach ($servers as $server) {
             $key = sprintf('%s:%d', $server->getHost(), $server->getPort());
             $key = sprintf('%s:%d', $server->getHost(), $server->getPort());
             $info = $server->getInfo();
             $info = $server->getInfo();
 
 
@@ -197,7 +206,7 @@ class MongoClient
                     $state = 0;
                     $state = 0;
             }
             }
 
 
-            $servers[$key] = [
+            $results[$key] = [
                 'host' => $server->getHost(),
                 'host' => $server->getHost(),
                 'port' => $server->getPort(),
                 'port' => $server->getPort(),
                 'health' => (int) $info['ok'],
                 'health' => (int) $info['ok'],
@@ -207,7 +216,7 @@ class MongoClient
             ];
             ];
         }
         }
 
 
-        return $servers;
+        return $results;
     }
     }
 
 
     /**
     /**
@@ -231,7 +240,11 @@ class MongoClient
      */
      */
     public function listDBs()
     public function listDBs()
     {
     {
-        $databaseInfoIterator = $this->client->listDatabases();
+        try {
+            $databaseInfoIterator = $this->client->listDatabases();
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
 
         $databases = [
         $databases = [
             'databases' => [],
             'databases' => [],

+ 210 - 47
lib/Mongo/MongoCollection.php

@@ -15,6 +15,7 @@
 
 
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\TypeConverter;
 use Alcaeus\MongoDbAdapter\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 
 
 /**
 /**
  * Represents a database collection.
  * Represents a database collection.
@@ -55,6 +56,7 @@ class MongoCollection
      */
      */
     public function __construct(MongoDB $db, $name)
     public function __construct(MongoDB $db, $name)
     {
     {
+        $this->checkCollectionName($name);
         $this->db = $db;
         $this->db = $db;
         $this->name = $name;
         $this->name = $name;
 
 
@@ -150,7 +152,12 @@ class MongoCollection
 
 
         $command += $options;
         $command += $options;
 
 
-        return $this->db->command($command);
+        try {
+            return $this->db->command($command);
+        } catch (MongoCursorTimeoutException $e) {
+            throw new MongoExecutionTimeoutException($e->getMessage(), $e->getCode(), $e);
+        }
+
     }
     }
 
 
     /**
     /**
@@ -257,14 +264,31 @@ class MongoCollection
     public function insert(&$a, array $options = [])
     public function insert(&$a, array $options = [])
     {
     {
         if (! $this->ensureDocumentHasMongoId($a)) {
         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;
             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()) {
         if (! $result->isAcknowledged()) {
             return true;
             return true;
@@ -289,14 +313,37 @@ class MongoCollection
      */
      */
     public function batchInsert(array &$a, array $options = [])
     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) {
         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()) {
         if (! $result->isAcknowledged()) {
             return true;
             return true;
@@ -328,12 +375,27 @@ class MongoCollection
         $method = $multiple ? 'updateMany' : 'updateOne';
         $method = $multiple ? 'updateMany' : 'updateOne';
         unset($options['multiple']);
         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()) {
         if (! $result->isAcknowledged()) {
             return true;
             return true;
@@ -365,11 +427,15 @@ class MongoCollection
         $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
         $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
         $method = $multiple ? 'deleteMany' : 'deleteOne';
         $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()) {
         if (! $result->isAcknowledged()) {
             return true;
             return true;
@@ -409,7 +475,11 @@ class MongoCollection
      */
      */
     public function distinct($key, array $query = [])
     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;
+        }
     }
     }
 
 
     /**
     /**
@@ -425,21 +495,28 @@ class MongoCollection
     public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
     public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
     {
     {
         $query = TypeConverter::fromLegacy($query);
         $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\RuntimeException $e) {
+            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
         }
         }
 
 
         if ($document) {
         if ($document) {
@@ -461,8 +538,12 @@ class MongoCollection
     public function findOne(array $query = [], array $fields = [], array $options = [])
     public function findOne(array $query = [], array $fields = [], array $options = [])
     {
     {
         $options = ['projection' => $fields] + $options;
         $options = ['projection' => $fields] + $options;
+        try {
+            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
 
-        $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
         if ($document !== null) {
         if ($document !== null) {
             $document = TypeConverter::toLegacy($document);
             $document = TypeConverter::toLegacy($document);
         }
         }
@@ -480,17 +561,63 @@ class MongoCollection
      *
      *
      * @todo This method does not yet return the correct result
      * @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];
+        }
 
 
-        return $this->collection->createIndex($keys, $options);
+        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 [
+                    '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
+        ];
     }
     }
 
 
     /**
     /**
@@ -570,7 +697,11 @@ class MongoCollection
      */
      */
     public function count($query = [], array $options = [])
     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);
+        }
     }
     }
 
 
     /**
     /**
@@ -594,7 +725,12 @@ class MongoCollection
 
 
         $options['upsert'] = true;
         $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;
     }
     }
 
 
     /**
     /**
@@ -756,21 +892,48 @@ class MongoCollection
      */
      */
     private function ensureDocumentHasMongoId(&$document)
     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 (is_array($document)) {
+            if (empty($document)) {
+                throw new \MongoException('document cannot be empty');
+            }
             if (! isset($document['_id'])) {
             if (! isset($document['_id'])) {
                 $document['_id'] = new \MongoId();
                 $document['_id'] = new \MongoId();
             }
             }
 
 
+            $checkKeys($document);
+
             return $document['_id'];
             return $document['_id'];
         } elseif (is_object($document)) {
         } elseif (is_object($document)) {
+            if (empty((array) $document)) {
+                throw new \MongoException('document cannot be empty');
+            }
             if (! isset($document->_id)) {
             if (! isset($document->_id)) {
                 $document->_id = new \MongoId();
                 $document->_id = new \MongoId();
             }
             }
 
 
+            $checkKeys((array) $document);
+
             return $document->_id;
             return $document->_id;
         }
         }
 
 
         return null;
         return null;
     }
     }
+
+    private function checkCollectionName($name)
+    {
+        if (empty($name)) {
+            throw new Exception('Collection name cannot be empty');
+        } elseif (strpos($name, chr(0)) !== false) {
+            throw new Exception('Collection name cannot contain null bytes');
+        }
+    }
 }
 }
 
 

+ 15 - 2
lib/Mongo/MongoCursor.php

@@ -15,6 +15,7 @@
 
 
 use Alcaeus\MongoDbAdapter\AbstractCursor;
 use Alcaeus\MongoDbAdapter\AbstractCursor;
 use Alcaeus\MongoDbAdapter\TypeConverter;
 use Alcaeus\MongoDbAdapter\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 use MongoDB\Driver\Cursor;
 use MongoDB\Driver\Cursor;
 use MongoDB\Driver\ReadPreference;
 use MongoDB\Driver\ReadPreference;
 use MongoDB\Operation\Find;
 use MongoDB\Operation\Find;
@@ -140,8 +141,14 @@ class MongoCursor extends AbstractCursor implements Iterator
         }
         }
 
 
         $options = $this->getOptions($optionNames) + $this->options;
         $options = $this->getOptions($optionNames) + $this->options;
+        try {
+            $count = $this->collection->count(TypeConverter::fromLegacy($this->query), $options);
+        } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
+            throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
 
-        $count = $this->collection->count(TypeConverter::fromLegacy($this->query), $options);
         return $count;
         return $count;
     }
     }
 
 
@@ -155,7 +162,13 @@ class MongoCursor extends AbstractCursor implements Iterator
     {
     {
         $options = $this->getOptions() + $this->options;
         $options = $this->getOptions() + $this->options;
 
 
-        $this->cursor = $this->collection->find(TypeConverter::fromLegacy($this->query), $options);
+        try {
+            $this->cursor = $this->collection->find(TypeConverter::fromLegacy($this->query), $options);
+        } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
+            throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
     }
     }
 
 
     /**
     /**

+ 43 - 3
lib/Mongo/MongoDB.php

@@ -15,6 +15,7 @@
 
 
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\TypeConverter;
 use Alcaeus\MongoDbAdapter\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 use MongoDB\Model\CollectionInfo;
 use MongoDB\Model\CollectionInfo;
 
 
 /**
 /**
@@ -58,6 +59,7 @@ class MongoDB
      */
      */
     public function __construct(MongoClient $conn, $name)
     public function __construct(MongoClient $conn, $name)
     {
     {
+        $this->checkDatabaseName($name);
         $this->connection = $conn;
         $this->connection = $conn;
         $this->name = $name;
         $this->name = $name;
 
 
@@ -130,7 +132,11 @@ class MongoDB
             unset($options['includeSystemCollections']);
             unset($options['includeSystemCollections']);
         }
         }
 
 
-        $collections = $this->db->listCollections($options);
+        try {
+            $collections = $this->db->listCollections($options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
 
         $getCollectionInfo = function (CollectionInfo $collectionInfo) {
         $getCollectionInfo = function (CollectionInfo $collectionInfo) {
             return [
             return [
@@ -156,7 +162,11 @@ class MongoDB
             unset($options['includeSystemCollections']);
             unset($options['includeSystemCollections']);
         }
         }
 
 
-        $collections = $this->db->listCollections($options);
+        try {
+            $collections = $this->db->listCollections($options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
 
         $getCollectionName = function (CollectionInfo $collectionInfo) {
         $getCollectionName = function (CollectionInfo $collectionInfo) {
             return $collectionInfo->getName();
             return $collectionInfo->getName();
@@ -260,7 +270,12 @@ class MongoDB
      */
      */
     public function createCollection($name, $options)
     public function createCollection($name, $options)
     {
     {
-        $this->db->createCollection($name, $options);
+        try {
+            $this->db->createCollection($name, $options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            return false;
+        }
+
         return $this->selectCollection($name);
         return $this->selectCollection($name);
     }
     }
 
 
@@ -366,12 +381,16 @@ class MongoDB
             $cursor->setReadPreference($this->getReadPreference());
             $cursor->setReadPreference($this->getReadPreference());
 
 
             return iterator_to_array($cursor)[0];
             return iterator_to_array($cursor)[0];
+        } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
+            throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
         } catch (\MongoDB\Driver\Exception\RuntimeException $e) {
         } catch (\MongoDB\Driver\Exception\RuntimeException $e) {
             return [
             return [
                 'ok' => 0,
                 'ok' => 0,
                 'errmsg' => $e->getMessage(),
                 'errmsg' => $e->getMessage(),
                 'code' => $e->getCode(),
                 'code' => $e->getCode(),
             ];
             ];
+        } catch (\MongoDB\Driver\Exception\Excepiton $e) {
+            ExceptionConverter::toLegacy($e);
         }
         }
     }
     }
 
 
@@ -477,4 +496,25 @@ class MongoDB
             $this->db = $this->db->withOptions($options);
             $this->db = $this->db->withOptions($options);
         }
         }
     }
     }
+
+    private function checkDatabaseName($name)
+    {
+        if (empty($name)) {
+            throw new \Exception('Database name cannot be empty');
+        }
+        if (strlen($name) >= 64) {
+            throw new \Exception('Database name cannot exceed 63 characters');
+        }
+        if (strpos($name, chr(0)) !== false) {
+            throw new \Exception('Database name cannot contain null bytes');
+        }
+
+        $invalidCharacters = ['.', '$', '/', ' ', '\\'];
+        foreach ($invalidCharacters as $char) {
+            if (strchr($name, $char) !== false) {
+                throw new \Exception('Database name contains invalid characters');
+            }
+        }
+
+    }
 }
 }

+ 63 - 9
lib/Mongo/MongoGridFS.php

@@ -201,8 +201,18 @@ class MongoGridFS extends MongoCollection
             'md5' => md5($bytes),
             'md5' => md5($bytes),
         ];
         ];
 
 
-        $file = $this->insertFile($record, $options);
-        $this->insertChunksFromBytes($bytes, $file);
+        try {
+            $file = $this->insertFile($record, $options);
+        } catch (MongoException $e) {
+            throw new MongoGridFSException('Cannot insert file record', 0, $e);
+        }
+
+        try {
+            $this->insertChunksFromBytes($bytes, $file);
+        } catch (MongoException $e) {
+            $this->delete($file['_id']);
+            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
+        }
 
 
         return $file['_id'];
         return $file['_id'];
     }
     }
@@ -241,8 +251,19 @@ class MongoGridFS extends MongoCollection
         }
         }
 
 
         $md5 = null;
         $md5 = null;
-        $file = $this->insertFile($record, $options);
-        $length = $this->insertChunksFromFile($handle, $file, $md5);
+        try {
+            $file = $this->insertFile($record, $options);
+        } catch (MongoException $e) {
+            throw new MongoGridFSException('Cannot insert file record', 0, $e);
+        }
+
+        try {
+            $length = $this->insertChunksFromFile($handle, $file, $md5);
+        } catch (MongoException $e) {
+            $this->delete($file['_id']);
+            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
+        }
+
 
 
         // Add length and MD5 if they were not present before
         // Add length and MD5 if they were not present before
         $update = [];
         $update = [];
@@ -250,11 +271,24 @@ class MongoGridFS extends MongoCollection
             $update['length'] = $length;
             $update['length'] = $length;
         }
         }
         if (! isset($record['md5'])) {
         if (! isset($record['md5'])) {
-            $update['md5'] = $md5;
+            try {
+                $update['md5'] = $md5;
+            } catch (MongoException $e) {
+                throw new MongoGridFSException('Error computing MD5 checksum', 0, $e);
+            }
         }
         }
 
 
         if (count($update)) {
         if (count($update)) {
-            $this->update(['_id' => $file['_id']], ['$set' => $update]);
+            try {
+                $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
+                if (! $this->isOKResult($result)) {
+                    throw new MongoGridFSException('Error updating file record');
+                }
+            } catch (MongoException $e) {
+                $this->delete($file['_id']);
+                throw new MongoGridFSException('Error updating file record', 0, $e);
+            }
+
         }
         }
 
 
         return $file['_id'];
         return $file['_id'];
@@ -300,7 +334,10 @@ class MongoGridFS extends MongoCollection
      */
      */
     private function createChunksIndex()
     private function createChunksIndex()
     {
     {
-        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
+        try {
+            $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
+        } catch (MongoDuplicateKeyException $e) {}
+
     }
     }
 
 
     /**
     /**
@@ -318,7 +355,14 @@ class MongoGridFS extends MongoCollection
             'n' => $chunkNumber,
             'n' => $chunkNumber,
             'data' => new MongoBinData($data),
             'data' => new MongoBinData($data),
         ];
         ];
-        return $this->chunks->insert($chunk);
+
+        $result = $this->chunks->insert($chunk);
+
+        if (! $this->isOKResult($result)) {
+            throw new \MongoException('error inserting chunk');
+        }
+
+        return $result;
     }
     }
 
 
     /**
     /**
@@ -387,8 +431,18 @@ class MongoGridFS extends MongoCollection
             'chunkSize' => self::DEFAULT_CHUNK_SIZE,
             'chunkSize' => self::DEFAULT_CHUNK_SIZE,
         ];
         ];
 
 
-        $this->insert($record, $options);
+        $result = $this->insert($record, $options);
+
+        if (! $this->isOKResult($result)) {
+            throw new \MongoException('error inserting file');
+        }
 
 
         return $record;
         return $record;
     }
     }
+
+    private function isOKResult($result)
+    {
+        return (is_array($result) && $result['ok'] == 1.0) ||
+               (is_bool($result) && $result);
+    }
 }
 }

+ 1 - 1
lib/Mongo/MongoGridFSCursor.php

@@ -65,6 +65,6 @@ class MongoGridFSCursor extends MongoCursor
     public function key()
     public function key()
     {
     {
         $file = $this->current();
         $file = $this->current();
-        return ($file !== null) ? $file->getFilename() : null;
+        return ($file !== null) ? (string)$file->file['_id'] : null;
     }
     }
 }
 }

+ 100 - 0
tests/Alcaeus/MongoDbAdapter/ExceptionConverterTest.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+use MongoDB\Driver\Exception;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
+
+class ExceptionConverterTest extends \PHPUnit_Framework_TestCase
+{
+    public function testThrowException()
+    {
+        $this->setExpectedException('MongoException');
+        ExceptionConverter::toLegacy(new Exception\InvalidArgumentException());
+    }
+
+    /**
+     * @dataProvider exceptionProvider
+     */
+    public function testConvertException($e, $expectedClass)
+    {
+        $exception = ExceptionConverter::convertException($e);
+        $this->assertInstanceOf($expectedClass, $exception);
+        $this->assertSame($e->getMessage(), $exception->getMessage());
+        $this->assertSame($e->getCode(), $exception->getCode());
+        $this->assertSame($e, $exception->getPrevious());
+    }
+
+    public function exceptionProvider()
+    {
+        return [
+            // Driver
+            [
+                new Exception\AuthenticationException('message', 1),
+                'MongoConnectionException',
+            ],
+            [
+                new Exception\BulkWriteException('message', 2),
+                'MongoCursorException',
+            ],
+            [
+                new Exception\ConnectionException('message', 2),
+                'MongoConnectionException',
+            ],
+            [
+                new Exception\ConnectionTimeoutException('message', 2),
+                'MongoConnectionException',
+            ],
+            [
+                new Exception\ExecutionTimeoutException('message', 2),
+                'MongoExecutionTimeoutException',
+            ],
+            [
+                new Exception\InvalidArgumentException('message', 2),
+                'MongoException',
+            ],
+            [
+                new Exception\LogicException('message', 2),
+                'MongoException',
+            ],
+            [
+                new Exception\RuntimeException('message', 2),
+                'MongoException',
+            ],
+            [
+                new Exception\SSLConnectionException('message', 2),
+                'MongoConnectionException',
+            ],
+            [
+                new Exception\UnexpectedValueException('message', 2),
+                'MongoException',
+            ],
+
+            // Library
+            [
+                new \MongoDB\Exception\BadMethodCallException('message', 2),
+                'MongoException',
+            ],
+            [
+                new \MongoDB\Exception\InvalidArgumentException('message', 2),
+                'MongoException',
+            ],
+            [
+                new \MongoDB\Exception\InvalidArgumentTypeException('message', 2, 'foo'),
+                'MongoException',
+            ],
+            [
+                new \MongoDB\Exception\UnexpectedTypeException('message', 2),
+                'MongoException',
+            ],
+            [
+                new \MongoDB\Exception\UnexpectedValueException('message', 2),
+                'MongoException',
+            ],
+            [
+                new \MongoDB\Exception\UnexpectedValueTypeException('message', 2, 'foo'),
+                'MongoException',
+            ],
+        ];
+    }
+}

+ 14 - 0
tests/Alcaeus/MongoDbAdapter/MongoClientTest.php

@@ -30,6 +30,20 @@ class MongoClientTest extends TestCase
         $this->assertSame('mongo-php-adapter', (string) $db);
         $this->assertSame('mongo-php-adapter', (string) $db);
     }
     }
 
 
+    public function testSelectDBWithEmptyName()
+    {
+        $this->setExpectedException('Exception', 'Database name cannot be empty');
+
+        $this->getClient()->selectDB('');
+    }
+
+    public function testSelectDBWithInvalidName()
+    {
+        $this->setExpectedException('Exception', 'Database name contains invalid characters');
+
+        $this->getClient()->selectDB('/');
+    }
+
     public function testGetDbProperty()
     public function testGetDbProperty()
     {
     {
         $client = $this->getClient();
         $client = $this->getClient();

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

@@ -41,12 +41,80 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('bar', 'foo', $object);
         $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()
     public function testUnacknowledgedWrite()
     {
     {
         $document = ['foo' => 'bar'];
         $document = ['foo' => 'bar'];
         $this->assertTrue($this->getCollection()->insert($document, ['w' => 0]));
         $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()
     public function testInsertMany()
     {
     {
         $expected = [
         $expected = [
@@ -69,6 +137,7 @@ class MongoCollectionTest extends TestCase
         }
         }
     }
     }
 
 
+
     public function testInsertManyWithNonNumericKeys()
     public function testInsertManyWithNonNumericKeys()
     {
     {
         $expected = [
         $expected = [
@@ -85,6 +154,53 @@ class MongoCollectionTest extends TestCase
             'b' => ['bar' => 'foo']
             'b' => ['bar' => 'foo']
         ];
         ];
         $this->assertSame($expected, $this->getCollection()->batchInsert($documents));
         $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()
     public function testUpdateOne()
@@ -111,6 +227,28 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(1, $this->getCheckDatabase()->selectCollection('test')->count(['foo' => 'foo']));
         $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()
     public function testUpdateMany()
     {
     {
         $document = ['change' => true, 'foo' => 'bar'];
         $document = ['change' => true, 'foo' => 'bar'];
@@ -220,6 +358,15 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(2, $collection->count(['foo' => 'bar']));
         $this->assertSame(2, $collection->count(['foo' => 'bar']));
     }
     }
 
 
+    public function testCountTimeout()
+    {
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $this->getCollection()->count([], ['maxTimeMS' => 1]);
+    }
+
     public function testFindOne()
     public function testFindOne()
     {
     {
         $this->prepareData();
         $this->prepareData();
@@ -228,6 +375,16 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(['foo' => 'foo'], $document);
         $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()
     public function testDistinct()
     {
     {
         $this->prepareData();
         $this->prepareData();
@@ -276,6 +433,29 @@ class MongoCollectionTest extends TestCase
         ], $result['result']);
         ], $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()
     public function testAggregateCursor()
     {
     {
         $collection = $this->getCollection();
         $collection = $this->getCollection();
@@ -369,12 +549,20 @@ class MongoCollectionTest extends TestCase
 
 
     public function testSaveInsert()
     public function testSaveInsert()
     {
     {
+        $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
         $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');
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
         $this->assertSame(1, $newCollection->count());
@@ -402,13 +590,22 @@ class MongoCollectionTest extends TestCase
 
 
     public function testSaveUpdate()
     public function testSaveUpdate()
     {
     {
+        $expected = [
+            'ok' => 1.0,
+            'nModified' => 1,
+            'n' => 1,
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => true,
+        ];
+
         $id = '54203e08d51d4a1f868b456e';
         $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
         $collection = $this->getCollection();
 
 
         $insertDocument = ['_id' => new \MongoId($id), 'foo' => 'bar'];
         $insertDocument = ['_id' => new \MongoId($id), 'foo' => 'bar'];
         $saveDocument = ['_id' => new \MongoId($id), 'foo' => 'foo'];
         $saveDocument = ['_id' => new \MongoId($id), 'foo' => 'foo'];
         $collection->insert($insertDocument);
         $collection->insert($insertDocument);
-        $collection->save($saveDocument);
+        $this->assertSame($expected, $collection->save($saveDocument));
 
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
         $this->assertSame(1, $newCollection->count());
@@ -421,6 +618,54 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('foo', 'foo', $object);
         $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()
     public function testGetDBRef()
     {
     {
         $collection = $this->getCollection();
         $collection = $this->getCollection();
@@ -450,8 +695,15 @@ class MongoCollectionTest extends TestCase
 
 
     public function testCreateIndex()
     public function testCreateIndex()
     {
     {
+        $expected = [
+            'createdCollectionAutomatically' => true,
+            'numIndexesBefore' => 1,
+            'numIndexesAfter' => 2,
+            'ok' => 1.0,
+        ];
+
         $collection = $this->getCollection();
         $collection = $this->getCollection();
-        $collection->createIndex(['foo' => 1]);
+        $this->assertSame($expected, $collection->createIndex(['foo' => 1]));
 
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $iterator = $newCollection->listIndexes();
         $iterator = $newCollection->listIndexes();
@@ -462,6 +714,45 @@ class MongoCollectionTest extends TestCase
         $this->assertSame('mongo-php-adapter.test', $index->getNamespace());
         $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()
     public function testEnsureIndex()
     {
     {
         $collection = $this->getCollection();
         $collection = $this->getCollection();
@@ -630,6 +921,37 @@ class MongoCollectionTest extends TestCase
         );
         );
     }
     }
 
 
+    public function testFindAndModifyResultException()
+    {
+        $collection = $this->getCollection();
+
+        $this->setExpectedException('MongoResultException');
+
+        $collection->findAndModify(
+            array("inprogress" => false, "name" => "Next promo"),
+            array('$unsupportedOperator' => array("tasks" => -1)),
+            array("tasks" => true),
+            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()
     public function testFindAndModifyRemove()
     {
     {
         $id = '54203e08d51d4a1f868b456e';
         $id = '54203e08d51d4a1f868b456e';
@@ -683,4 +1005,27 @@ class MongoCollectionTest extends TestCase
         ];
         ];
         $this->assertSame($expected, $this->getCollection()->drop());
         $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)};
+    }
 }
 }

+ 10 - 0
tests/Alcaeus/MongoDbAdapter/MongoCursorTest.php

@@ -39,6 +39,16 @@ class MongoCursorTest extends TestCase
         $this->assertSame(1, $cursor->count(true));
         $this->assertSame(1, $cursor->count(true));
     }
     }
 
 
+    public function testCountCannotConnect()
+    {
+        $client = $this->getClient([], 'mongodb://localhost:28888');
+        $cursor = $client->selectCollection('mongo-php-adapter', 'test')->find();
+
+        $this->setExpectedException('MongoConnectionException');
+
+        $cursor->count();
+    }
+
     /**
     /**
      * @dataProvider getCursorOptions
      * @dataProvider getCursorOptions
      */
      */

+ 109 - 0
tests/Alcaeus/MongoDbAdapter/MongoDBTest.php

@@ -8,6 +8,20 @@ use MongoDB\Driver\ReadPreference;
  */
  */
 class MongoDBTest extends TestCase
 class MongoDBTest extends TestCase
 {
 {
+    public function testEmptyDatabaseName()
+    {
+        $this->setExpectedException('Exception', 'Database name cannot be empty');
+
+        new \MongoDB($this->getClient(), '');
+    }
+
+    public function testInvalidDatabaseName()
+    {
+        $this->setExpectedException('Exception', 'Database name contains invalid characters');
+
+        new \MongoDB($this->getClient(), '/');
+    }
+
     public function testGetCollection()
     public function testGetCollection()
     {
     {
         $db = $this->getDatabase();
         $db = $this->getDatabase();
@@ -16,6 +30,50 @@ class MongoDBTest extends TestCase
         $this->assertSame('mongo-php-adapter.test', (string) $collection);
         $this->assertSame('mongo-php-adapter.test', (string) $collection);
     }
     }
 
 
+    public function testSelectCollectionEmptyName()
+    {
+        $database = $this->getDatabase();
+
+        $this->setExpectedException('Exception', 'Collection name cannot be empty');
+
+        $database->selectCollection('');
+    }
+
+    public function testSelectCollectionWithNullBytes()
+    {
+        $database = $this->getDatabase();
+
+        $this->setExpectedException('Exception', 'Collection name cannot contain null bytes');
+
+        $database->selectCollection('foo' . chr(0));
+    }
+
+    public function testCreateCollection()
+    {
+        $database = $this->getDatabase();
+
+        $collection = $database->createCollection('test', ['capped' => true, 'size' => 100]);
+        $this->assertInstanceOf('MongoCollection', $collection);
+
+        $document = ['foo' => 'bar'];
+        $collection->insert($document);
+
+        $checkDatabase = $this->getCheckDatabase();
+        foreach ($checkDatabase->listCollections() as $collectionInfo) {
+            if ($collectionInfo->getName() === 'test') {
+                $this->assertTrue($collectionInfo->isCapped());
+                return;
+            }
+        }
+    }
+
+    public function testCreateCollectionInvalidParameters()
+    {
+        $database = $this->getDatabase();
+
+        $this->assertFalse($database->createCollection('test', ['capped' => 2, 'size' => 100]));
+    }
+
     public function testGetCollectionProperty()
     public function testGetCollectionProperty()
     {
     {
         $db = $this->getDatabase();
         $db = $this->getDatabase();
@@ -42,6 +100,21 @@ class MongoDBTest extends TestCase
         $this->assertEquals($expected, $db->command(['listDatabases' => 1]));
         $this->assertEquals($expected, $db->command(['listDatabases' => 1]));
     }
     }
 
 
+    public function testCommandCursorTimeout()
+    {
+        $database = $this->getDatabase();
+
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoCursorTimeoutException');
+
+        $database->command([
+            "count" => "test",
+            "query" => array("a" => 1),
+            "maxTimeMS" => 100,
+        ]);
+    }
+
     public function testReadPreference()
     public function testReadPreference()
     {
     {
         $database = $this->getDatabase();
         $database = $this->getDatabase();
@@ -137,6 +210,19 @@ class MongoDBTest extends TestCase
         $this->assertContains('test', $this->getDatabase()->getCollectionNames());
         $this->assertContains('test', $this->getDatabase()->getCollectionNames());
     }
     }
 
 
+    public function testGetCollectionNamesExecutionTimeoutException()
+    {
+        $document = ['foo' => 'bar'];
+        $this->getCollection()->insert($document);
+        $database = $this->getDatabase();
+
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $database->getCollectionNames(['maxTimeMS' => 1]);
+    }
+
     public function testGetCollectionInfo()
     public function testGetCollectionInfo()
     {
     {
         $document = ['foo' => 'bar'];
         $document = ['foo' => 'bar'];
@@ -152,6 +238,20 @@ class MongoDBTest extends TestCase
         $this->fail('The test collection was not found');
         $this->fail('The test collection was not found');
     }
     }
 
 
+    public function testGetCollectionInfoExecutionTimeoutException()
+    {
+        $document = ['foo' => 'bar'];
+        $this->getCollection()->insert($document);
+
+        $database = $this->getDatabase();
+
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $database->getCollectionInfo(['maxTimeMS' => 1]);
+    }
+
     public function testListCollections()
     public function testListCollections()
     {
     {
         $document = ['foo' => 'bar'];
         $document = ['foo' => 'bar'];
@@ -167,6 +267,15 @@ class MongoDBTest extends TestCase
         $this->fail('The test collection was not found');
         $this->fail('The test collection was not found');
     }
     }
 
 
+    public function testListCollectionsExecutionTimeoutException()
+    {
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $this->getDatabase()->listCollections(['maxTimeMS' => 1]);
+    }
+
     public function testDrop()
     public function testDrop()
     {
     {
         $document = ['foo' => 'bar'];
         $document = ['foo' => 'bar'];

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

@@ -7,13 +7,13 @@ class MongoGridFSCursorTest extends TestCase
     public function testCursorItems()
     public function testCursorItems()
     {
     {
         $gridfs = $this->getGridFS();
         $gridfs = $this->getGridFS();
-        $gridfs->storeBytes('foo', ['filename' => 'foo.txt']);
+        $id = $gridfs->storeBytes('foo', ['filename' => 'foo.txt']);
         $gridfs->storeBytes('bar', ['filename' => 'bar.txt']);
         $gridfs->storeBytes('bar', ['filename' => 'bar.txt']);
 
 
         $cursor = $gridfs->find(['filename' => 'foo.txt']);
         $cursor = $gridfs->find(['filename' => 'foo.txt']);
         $this->assertCount(1, $cursor);
         $this->assertCount(1, $cursor);
         foreach ($cursor as $key => $value) {
         foreach ($cursor as $key => $value) {
-            $this->assertSame('foo.txt', $key);
+            $this->assertSame((string)$id, $key);
             $this->assertInstanceOf('MongoGridFSFile', $value);
             $this->assertInstanceOf('MongoGridFSFile', $value);
             $this->assertSame('foo', $value->getBytes());
             $this->assertSame('foo', $value->getBytes());
 
 

+ 66 - 0
tests/Alcaeus/MongoDbAdapter/MongoGridFSTest.php

@@ -294,6 +294,72 @@ class MongoGridFSTest extends TestCase
         $this->assertSame($numberOfChunks, $newChunksCollection->count());
         $this->assertSame($numberOfChunks, $newChunksCollection->count());
     }
     }
 
 
+    public function testStoreByteExceptionWhileInsertingRecord()
+    {
+        $id = new \MongoID();
+
+        $collection = $this->getGridFS();
+
+        $document = ['_id' => $id];
+        $collection->insert($document);
+
+        $this->setExpectedException('MongoGridFSException', 'Cannot insert file record');
+
+        $collection->storeBytes('foo', ['_id' => $id]);
+    }
+
+    public function testStoreByteExceptionWhileInsertingChunks()
+    {
+        $collection = $this->getGridFS();
+        $collection->chunks->createIndex(['n' => 1], ['unique' => true]);
+
+        $document = ['n' => 0];
+        $collection->chunks->insert($document);
+
+        $this->setExpectedException('MongoGridFSException', 'Error while inserting chunks');
+
+        $collection->storeBytes('foo');
+    }
+
+    public function testStoreFileExceptionWhileInsertingRecord()
+    {
+        $id = new \MongoID();
+
+        $collection = $this->getGridFS();
+        $document = ['_id' => $id];
+        $collection->insert($document);
+
+        $this->setExpectedException('MongoGridFSException', 'Cannot insert file record');
+
+        $collection->storeFile(__FILE__, ['_id' => $id]);
+    }
+
+    public function testStoreFileExceptionWhileInsertingChunks()
+    {
+        $collection = $this->getGridFS();
+        $collection->chunks->createIndex(['n' => 1], ['unique' => true]);
+
+        $document = ['n' => 0];
+        $collection->chunks->insert($document);
+
+        $this->setExpectedException('MongoGridFSException', 'Error while inserting chunks');
+
+        $collection->storeFile(__FILE__);
+    }
+
+    public function testStoreFileExceptionWhileUpdatingFileRecord()
+    {
+        $collection = $this->getGridFS();
+        $collection->createIndex(['length' => 1], ['unique' => true]);
+
+        $document = ['length' => filesize(__FILE__)];
+        $collection->insert($document);
+
+        $this->setExpectedException('MongoGridFSException', 'Error updating file record');
+
+        $collection->storeFile(fopen(__FILE__, 'r'));
+    }
+
     /**
     /**
      * @var \MongoID
      * @var \MongoID
      */
      */

+ 53 - 3
tests/Alcaeus/MongoDbAdapter/TestCase.php

@@ -12,11 +12,19 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
     }
     }
 
 
     /**
     /**
+     * @return \MongoDB\Client
+     */
+    protected function getCheckClient()
+    {
+        return new Client('mongodb://localhost', ['connect' => true]);
+    }
+
+    /**
      * @return \MongoDB\Database
      * @return \MongoDB\Database
      */
      */
     protected function getCheckDatabase()
     protected function getCheckDatabase()
     {
     {
-        $client = new Client('mongodb://localhost', ['connect' => true]);
+        $client = $this->getCheckClient();
         return $client->selectDatabase('mongo-php-adapter');
         return $client->selectDatabase('mongo-php-adapter');
     }
     }
 
 
@@ -24,9 +32,9 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
      * @param array|null $options
      * @param array|null $options
      * @return \MongoClient
      * @return \MongoClient
      */
      */
-    protected function getClient($options = null)
+    protected function getClient($options = null, $uri = 'mongodb://localhost')
     {
     {
-        $args = ['mongodb://localhost'];
+        $args = [$uri];
         if ($options !== null) {
         if ($options !== null) {
             $args[] = $options;
             $args[] = $options;
         }
         }
@@ -95,4 +103,46 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
 
 
         return $collection;
         return $collection;
     }
     }
+
+    protected function configureFailPoint($failPoint, $mode, $data = [])
+    {
+        $this->checkFailPoint();
+
+        $doc = array(
+            "configureFailPoint" => $failPoint,
+            "mode"               => $mode,
+        );
+        if ($data) {
+            $doc["data"] = $data;
+        }
+
+        $adminDb = $this->getCheckClient()->selectDatabase('admin');
+        $result = $adminDb->command($doc);
+        $arr = current($result->toArray());
+        if (empty($arr->ok)) {
+            throw new RuntimeException("Failpoint failed");
+        }
+
+        return true;
+    }
+
+    protected function checkFailPoint()
+    {
+        $database = $this->getCheckClient()->selectDatabase('test');
+        try {
+            $database->command(['configureFailPoint' => 1]);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            /* command not found */
+            if ($e->getCode() == 59) {
+                $this->markTestSkipped(
+                  'This test require the mongo daemon to be started with the test flag: --setParameter enableTestCommands=1'
+                );
+            }
+        }
+    }
+
+    protected function failMaxTimeMS()
+    {
+        return $this->configureFailPoint("maxTimeAlwaysTimeOut", array("times" => 1));
+    }
 }
 }