Browse Source

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 years ago
parent
commit
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\ExceptionConverter;
 use MongoDB\Client;
 
 /**
@@ -91,6 +92,7 @@ class MongoClient
         }
     }
 
+
     /**
      * Closes this database connection
      *
@@ -181,8 +183,15 @@ class MongoClient
     {
         $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());
             $info = $server->getInfo();
 
@@ -197,7 +206,7 @@ class MongoClient
                     $state = 0;
             }
 
-            $servers[$key] = [
+            $results[$key] = [
                 'host' => $server->getHost(),
                 'port' => $server->getPort(),
                 'health' => (int) $info['ok'],
@@ -207,7 +216,7 @@ class MongoClient
             ];
         }
 
-        return $servers;
+        return $results;
     }
 
     /**
@@ -231,7 +240,11 @@ class MongoClient
      */
     public function listDBs()
     {
-        $databaseInfoIterator = $this->client->listDatabases();
+        try {
+            $databaseInfoIterator = $this->client->listDatabases();
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
+        }
 
         $databases = [
             'databases' => [],

+ 210 - 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.
@@ -55,6 +56,7 @@ class MongoCollection
      */
     public function __construct(MongoDB $db, $name)
     {
+        $this->checkCollectionName($name);
         $this->db = $db;
         $this->name = $name;
 
@@ -150,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);
+        }
+
     }
 
     /**
@@ -257,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;
@@ -289,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;
@@ -328,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;
@@ -365,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;
@@ -409,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;
+        }
     }
 
     /**
@@ -425,21 +495,28 @@ 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\RuntimeException $e) {
+            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            ExceptionConverter::toLegacy($e);
         }
 
         if ($document) {
@@ -461,8 +538,12 @@ 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) {
+            ExceptionConverter::toLegacy($e);
+        }
 
-        $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
         if ($document !== null) {
             $document = TypeConverter::toLegacy($document);
         }
@@ -480,17 +561,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];
+        }
 
-        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 = [])
     {
-        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;
 
-        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)
     {
+        $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;
         }
 
         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\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 use MongoDB\Driver\Cursor;
 use MongoDB\Driver\ReadPreference;
 use MongoDB\Operation\Find;
@@ -140,8 +141,14 @@ class MongoCursor extends AbstractCursor implements Iterator
         }
 
         $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;
     }
 
@@ -155,7 +162,13 @@ class MongoCursor extends AbstractCursor implements Iterator
     {
         $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\TypeConverter;
+use Alcaeus\MongoDbAdapter\ExceptionConverter;
 use MongoDB\Model\CollectionInfo;
 
 /**
@@ -58,6 +59,7 @@ class MongoDB
      */
     public function __construct(MongoClient $conn, $name)
     {
+        $this->checkDatabaseName($name);
         $this->connection = $conn;
         $this->name = $name;
 
@@ -130,7 +132,11 @@ class MongoDB
             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) {
             return [
@@ -156,7 +162,11 @@ class MongoDB
             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) {
             return $collectionInfo->getName();
@@ -260,7 +270,12 @@ class MongoDB
      */
     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);
     }
 
@@ -366,12 +381,16 @@ class MongoDB
             $cursor->setReadPreference($this->getReadPreference());
 
             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) {
             return [
                 'ok' => 0,
                 'errmsg' => $e->getMessage(),
                 'code' => $e->getCode(),
             ];
+        } catch (\MongoDB\Driver\Exception\Excepiton $e) {
+            ExceptionConverter::toLegacy($e);
         }
     }
 
@@ -477,4 +496,25 @@ class MongoDB
             $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),
         ];
 
-        $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'];
     }
@@ -241,8 +251,19 @@ class MongoGridFS extends MongoCollection
         }
 
         $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
         $update = [];
@@ -250,11 +271,24 @@ class MongoGridFS extends MongoCollection
             $update['length'] = $length;
         }
         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)) {
-            $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'];
@@ -300,7 +334,10 @@ class MongoGridFS extends MongoCollection
      */
     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,
             '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,
         ];
 
-        $this->insert($record, $options);
+        $result = $this->insert($record, $options);
+
+        if (! $this->isOKResult($result)) {
+            throw new \MongoException('error inserting file');
+        }
 
         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()
     {
         $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);
     }
 
+    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()
     {
         $client = $this->getClient();

+ 351 - 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,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()
     {
         $id = '54203e08d51d4a1f868b456e';
@@ -683,4 +1005,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)};
+    }
 }

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

@@ -39,6 +39,16 @@ class MongoCursorTest extends TestCase
         $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
      */

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

@@ -8,6 +8,20 @@ use MongoDB\Driver\ReadPreference;
  */
 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()
     {
         $db = $this->getDatabase();
@@ -16,6 +30,50 @@ class MongoDBTest extends TestCase
         $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()
     {
         $db = $this->getDatabase();
@@ -42,6 +100,21 @@ class MongoDBTest extends TestCase
         $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()
     {
         $database = $this->getDatabase();
@@ -137,6 +210,19 @@ class MongoDBTest extends TestCase
         $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()
     {
         $document = ['foo' => 'bar'];
@@ -152,6 +238,20 @@ class MongoDBTest extends TestCase
         $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()
     {
         $document = ['foo' => 'bar'];
@@ -167,6 +267,15 @@ class MongoDBTest extends TestCase
         $this->fail('The test collection was not found');
     }
 
+    public function testListCollectionsExecutionTimeoutException()
+    {
+        $this->failMaxTimeMS();
+
+        $this->setExpectedException('MongoExecutionTimeoutException');
+
+        $this->getDatabase()->listCollections(['maxTimeMS' => 1]);
+    }
+
     public function testDrop()
     {
         $document = ['foo' => 'bar'];

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

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

+ 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
      */
     protected function getCheckDatabase()
     {
-        $client = new Client('mongodb://localhost', ['connect' => true]);
+        $client = $this->getCheckClient();
         return $client->selectDatabase('mongo-php-adapter');
     }
 
@@ -24,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;
         }
@@ -95,4 +103,46 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
 
         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));
+    }
 }