diff --git a/Cargo.toml b/Cargo.toml index 8d37eefb6..477ab9f16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ bson = { git = "https://github.com/mongodb/bson-rust", branch = "main" } chrono = { version = "0.4.7", default-features = false, features = ["clock", "std"] } derivative = "2.1.1" flate2 = { version = "1.0", optional = true } +futures-io = "0.3.21" futures-core = "0.3.14" futures-util = { version = "0.3.14", features = ["io"] } futures-executor = "0.3.14" @@ -150,7 +151,7 @@ features = ["dangerous_configuration"] [dependencies.tokio-util] version = "0.7.0" -features = ["io"] +features = ["io", "compat"] [dependencies.uuid] version = "1.1.2" @@ -163,6 +164,7 @@ ctrlc = "3.2.2" derive_more = "0.99.13" function_name = "0.2.1" futures = "0.3" +hex = "0.4" home = "0.5" lambda_runtime = "0.6.0" pretty_assertions = "1.3.0" diff --git a/src/client/csfle/state_machine.rs b/src/client/csfle/state_machine.rs index f08a3b771..144311592 100644 --- a/src/client/csfle/state_machine.rs +++ b/src/client/csfle/state_machine.rs @@ -184,7 +184,7 @@ impl CryptExecutor { State::NeedKmsCredentials => { // TODO(RUST-1314, RUST-1417): support fetching KMS credentials. return Err(Error::internal("KMS credentials are not yet supported")); - }, + } State::Ready => { let (tx, rx) = oneshot::channel(); let mut thread_ctx = std::mem::replace( diff --git a/src/db/mod.rs b/src/db/mod.rs index 71e9a92c8..8a02dfa89 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -19,12 +19,7 @@ use crate::{ concern::{ReadConcern, WriteConcern}, cursor::Cursor, error::{Error, ErrorKind, Result}, - gridfs::{ - options::GridFsBucketOptions, - GridFsBucket, - DEFAULT_BUCKET_NAME, - DEFAULT_CHUNK_SIZE_BYTES, - }, + gridfs::{options::GridFsBucketOptions, GridFsBucket}, operation::{Aggregate, AggregateTarget, Create, DropDatabase, ListCollections, RunCommand}, options::{ AggregateOptions, @@ -573,23 +568,6 @@ impl Database { /// Creates a new GridFsBucket in the database with the given options. pub fn gridfs_bucket(&self, options: impl Into>) -> GridFsBucket { - let mut options = options.into().unwrap_or_default(); - options.read_concern = options - .read_concern - .or_else(|| self.read_concern().cloned()); - options.write_concern = options - .write_concern - .or_else(|| self.write_concern().cloned()); - options.selection_criteria = options - .selection_criteria - .or_else(|| self.selection_criteria().cloned()); - options.bucket_name = options - .bucket_name - .or_else(|| Some(DEFAULT_BUCKET_NAME.to_string())); - options.chunk_size_bytes = options.chunk_size_bytes.or(Some(DEFAULT_CHUNK_SIZE_BYTES)); - GridFsBucket { - db: self.clone(), - options, - } + GridFsBucket::new(self.clone(), options.into().unwrap_or_default()) } } diff --git a/src/error.rs b/src/error.rs index 931630fe4..bc8e3f4b7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -457,6 +457,11 @@ pub enum ErrorKind { #[non_exhaustive] DnsResolve { message: String }, + /// A GridFS error occurred. + #[error("{0:?}")] + #[non_exhaustive] + GridFs(GridFsErrorKind), + #[error("Internal error: {message}")] #[non_exhaustive] Internal { message: String }, @@ -693,6 +698,49 @@ impl WriteFailure { } } +/// An error that occurred during a GridFS operation. +#[derive(Clone, Debug)] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum GridFsErrorKind { + /// The file with the given identifier was not found. + #[non_exhaustive] + FileNotFound { identifier: GridFsFileIdentifier }, + + /// The file with the given revision was not found. + #[non_exhaustive] + RevisionNotFound { revision: i32 }, + + /// The chunk at index 'n' was missing. + #[non_exhaustive] + MissingChunk { n: u32 }, + + /// The chunk was the incorrect size. + #[non_exhaustive] + WrongSizeChunk { + actual_size: u32, + expected_size: u32, + }, + + /// An incorrect number of chunks was present for the file. + #[non_exhaustive] + WrongNumberOfChunks { + actual_number: u32, + expected_number: u32, + }, +} + +/// An identifier for a file stored in a GridFS bucket. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum GridFsFileIdentifier { + /// The name of the file. Not guaranteed to be unique. + Filename(String), + + /// The file's unique [`Bson`] ID. + Id(Bson), +} + /// Translates ErrorKind::BulkWriteError cases to ErrorKind::WriteErrors, leaving all other errors /// untouched. pub(crate) fn convert_bulk_errors(error: Error) -> Error { diff --git a/src/gridfs.rs b/src/gridfs.rs index 9b58e3b37..71431d5ae 100644 --- a/src/gridfs.rs +++ b/src/gridfs.rs @@ -1,52 +1,72 @@ -#![allow(dead_code, unused_variables)] // TODO(RUST-1395) Remove these allows. +#![allow(dead_code, unused_variables)] +mod download; pub mod options; use core::task::{Context, Poll}; -use std::pin::Pin; +use std::{ + pin::Pin, + sync::{atomic::AtomicBool, Arc}, +}; + +use serde::{Deserialize, Serialize}; +use tokio::io::ReadBuf; use crate::{ + bson::{doc, oid::ObjectId, Bson, DateTime, Document, RawBinaryRef}, concern::{ReadConcern, WriteConcern}, cursor::Cursor, error::Result, - selection_criteria::SelectionCriteria, + options::SelectionCriteria, + Collection, Database, }; -use bson::{oid::ObjectId, Bson, DateTime, Document}; + use options::*; -use serde::{Deserialize, Serialize}; -use tokio::io::ReadBuf; pub const DEFAULT_BUCKET_NAME: &str = "fs"; pub const DEFAULT_CHUNK_SIZE_BYTES: u32 = 255 * 1024; // Contained in a "chunks" collection for each user file -struct Chunk { +#[derive(Debug, Deserialize, Serialize)] +struct Chunk<'a> { + #[serde(rename = "_id")] id: ObjectId, files_id: Bson, n: u32, - // default size is 255 KiB - data: Vec, + #[serde(borrow)] + data: RawBinaryRef<'a>, } /// A collection in which information about stored files is stored. There will be one files /// collection document per stored file. -#[derive(Serialize, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct FilesCollectionDocument { - id: Bson, - length: i64, - chunk_size: u32, - upload_date: DateTime, - filename: String, - metadata: Document, + #[serde(rename = "_id")] + pub id: Bson, + pub length: u64, + pub chunk_size: u32, + pub upload_date: DateTime, + pub filename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +#[derive(Debug)] +struct GridFsBucketInner { + options: GridFsBucketOptions, + files: Collection, + chunks: Collection>, + created_indexes: AtomicBool, } /// Struct for storing GridFS managed files within a [`Database`]. +#[derive(Debug, Clone)] pub struct GridFsBucket { - // Contains a "chunks" collection - pub(crate) db: Database, - pub(crate) options: GridFsBucketOptions, + inner: Arc, } // TODO: RUST-1395 Add documentation and example code for this struct. @@ -134,30 +154,67 @@ impl tokio::io::AsyncRead for GridFsDownloadStream { } } -impl futures_util::io::AsyncRead for GridFsDownloadStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - todo!() +impl GridFsBucket { + pub(crate) fn new(db: Database, mut options: GridFsBucketOptions) -> GridFsBucket { + if options.read_concern.is_none() { + options.read_concern = db.read_concern().cloned(); + } + if options.write_concern.is_none() { + options.write_concern = db.write_concern().cloned(); + } + if options.selection_criteria.is_none() { + options.selection_criteria = db.selection_criteria().cloned(); + } + + let bucket_name = options + .bucket_name + .as_deref() + .unwrap_or(DEFAULT_BUCKET_NAME); + + let files = db.collection::(&format!("{}.files", bucket_name)); + let chunks = db.collection::(&format!("{}.chunks", bucket_name)); + + GridFsBucket { + inner: Arc::new(GridFsBucketInner { + options, + files, + chunks, + created_indexes: AtomicBool::new(false), + }), + } } -} -impl GridFsBucket { /// Gets the read concern of the [`GridFsBucket`]. pub fn read_concern(&self) -> Option<&ReadConcern> { - self.options.read_concern.as_ref() + self.inner.options.read_concern.as_ref() } /// Gets the write concern of the [`GridFsBucket`]. pub fn write_concern(&self) -> Option<&WriteConcern> { - self.options.write_concern.as_ref() + self.inner.options.write_concern.as_ref() } /// Gets the selection criteria of the [`GridFsBucket`]. pub fn selection_criteria(&self) -> Option<&SelectionCriteria> { - self.options.selection_criteria.as_ref() + self.inner.options.selection_criteria.as_ref() + } + + /// Gets the chunk size in bytes for the [`GridFsBucket`]. + fn chunk_size_bytes(&self) -> u32 { + self.inner + .options + .chunk_size_bytes + .unwrap_or(DEFAULT_CHUNK_SIZE_BYTES) + } + + /// Gets a handle to the files collection for the [`GridFsBucket`]. + fn files(&self) -> &Collection { + &self.inner.files + } + + /// Gets a handle to the chunks collection for the [`GridFsBucket`]. + fn chunks(&self) -> &Collection { + &self.inner.chunks } /// Opens a [`GridFsUploadStream`] that the application can write the contents of the file to. @@ -173,19 +230,6 @@ impl GridFsBucket { todo!() } - /// Opens a [`GridFsUploadStream`] that the application can write the contents of the file to. - /// The driver generates a unique [`Bson::ObjectId`] for the file id. - /// - /// Returns a [`GridFsUploadStream`] to which the application will write the contents. - pub async fn open_upload_stream( - &self, - filename: String, - options: impl Into>, - ) -> Result { - self.open_upload_stream_with_id(Bson::ObjectId(ObjectId::new()), filename, options) - .await - } - /// Uploads a user file to a GridFS bucket. The application supplies a custom file id. Uses the /// `tokio` crate's `AsyncRead` trait for the `source`. pub async fn upload_from_tokio_reader_with_id( @@ -244,6 +288,19 @@ impl GridFsBucket { .await } + /// Opens a [`GridFsUploadStream`] that the application can write the contents of the file to. + /// The driver generates a unique [`Bson::ObjectId`] for the file id. + /// + /// Returns a [`GridFsUploadStream`] to which the application will write the contents. + pub async fn open_upload_stream( + &self, + filename: String, + options: impl Into>, + ) -> Result { + self.open_upload_stream_with_id(Bson::ObjectId(ObjectId::new()), filename, options) + .await + } + /// Opens and returns a [`GridFsDownloadStream`] from which the application can read /// the contents of the stored file specified by `id`. pub async fn open_download_stream(&self, id: Bson) -> Result { @@ -261,52 +318,6 @@ impl GridFsBucket { todo!() } - /// Downloads the contents of the stored file specified by `id` and writes - /// the contents to the `destination`. Uses the `tokio` crate's `AsyncWrite` - /// trait for the `destination`. - pub async fn download_to_tokio_writer( - &self, - id: Bson, - destination: impl tokio::io::AsyncWrite, - ) { - todo!() - } - - /// Downloads the contents of the stored file specified by `id` and writes - /// the contents to the `destination`. Uses the `futures-0.3` crate's `AsyncWrite` - /// trait for the `destination`. - pub async fn download_to_futures_0_3_writer( - &self, - id: Bson, - destination: impl futures_util::AsyncWrite, - ) { - todo!() - } - - /// Downloads the contents of the stored file specified by `filename` and by - /// the revision in `options` and writes the contents to the `destination`. Uses the - /// `tokio` crate's `AsyncWrite` trait for the `destination`. - pub async fn download_to_tokio_writer_by_name( - &self, - filename: String, - destination: impl tokio::io::AsyncWrite, - options: impl Into>, - ) { - todo!() - } - - /// Downloads the contents of the stored file specified by `filename` and by - /// the revision in `options` and writes the contents to the `destination`. Uses the - /// `futures-0.3` crate's `AsyncWrite` trait for the `destination`. - pub async fn download_to_futures_0_3_writer_by_name( - &self, - filename: String, - destination: impl futures_util::AsyncWrite, - options: impl Into>, - ) { - todo!() - } - /// Given an `id`, deletes the stored file's files collection document and /// associated chunks from a [`GridFsBucket`]. pub async fn delete(&self, id: Bson) { diff --git a/src/gridfs/download.rs b/src/gridfs/download.rs new file mode 100644 index 000000000..b5f093aad --- /dev/null +++ b/src/gridfs/download.rs @@ -0,0 +1,154 @@ +use std::marker::Unpin; + +use futures_util::io::{AsyncWrite, AsyncWriteExt}; + +use super::{options::GridFsDownloadByNameOptions, FilesCollectionDocument, GridFsBucket}; +use crate::{ + bson::{doc, Bson}, + error::{ErrorKind, GridFsErrorKind, GridFsFileIdentifier, Result}, + options::{FindOneOptions, FindOptions}, +}; + +impl GridFsBucket { + /// Downloads the contents of the stored file specified by `id` and writes + /// the contents to the `destination`. + pub async fn download_to_futures_0_3_writer(&self, id: Bson, destination: T) -> Result<()> + where + T: AsyncWrite + Unpin, + { + let options = FindOneOptions::builder() + .read_concern(self.read_concern().cloned()) + .selection_criteria(self.selection_criteria().cloned()) + .build(); + + let file = match self + .inner + .files + .find_one(doc! { "_id": id.clone() }, options) + .await? + { + Some(fcd) => fcd, + None => { + return Err(ErrorKind::GridFs(GridFsErrorKind::FileNotFound { + identifier: GridFsFileIdentifier::Id(id), + }) + .into()); + } + }; + self.download_to_writer_common(file, destination).await + } + + /// Downloads the contents of the stored file specified by `filename` and writes the contents to + /// the `destination`. If there are multiple files with the same filename, the `revision` in the + /// options provided is used to determine which one to download. If no `revision` is specified, + /// the most recent file with the given filename is chosen. + pub async fn download_to_futures_0_3_writer_by_name( + &self, + filename: impl AsRef, + destination: T, + options: impl Into>, + ) -> Result<()> + where + T: AsyncWrite + Unpin, + { + let revision = options.into().and_then(|opts| opts.revision).unwrap_or(-1); + let (sort, skip) = if revision >= 0 { + (1, revision) + } else { + (-1, -revision - 1) + }; + let options = FindOneOptions::builder() + .sort(doc! { "uploadDate": sort }) + .skip(skip as u64) + .read_concern(self.read_concern().cloned()) + .selection_criteria(self.selection_criteria().cloned()) + .build(); + + let file = match self + .files() + .find_one(doc! { "filename": filename.as_ref() }, options) + .await? + { + Some(fcd) => fcd, + None => { + if self + .files() + .find_one(doc! { "filename": filename.as_ref() }, None) + .await? + .is_some() + { + return Err( + ErrorKind::GridFs(GridFsErrorKind::RevisionNotFound { revision }).into(), + ); + } else { + return Err(ErrorKind::GridFs(GridFsErrorKind::FileNotFound { + identifier: GridFsFileIdentifier::Filename(filename.as_ref().into()), + }) + .into()); + } + } + }; + + self.download_to_writer_common(file, destination).await + } + + async fn download_to_writer_common( + &self, + file: FilesCollectionDocument, + mut destination: T, + ) -> Result<()> + where + T: AsyncWrite + Unpin, + { + let total_bytes_expected = file.length; + let chunk_size = file.chunk_size as u64; + + if total_bytes_expected == 0 { + return Ok(()); + } + + let options = FindOptions::builder() + .sort(doc! { "n": 1 }) + .read_concern(self.read_concern().cloned()) + .selection_criteria(self.selection_criteria().cloned()) + .build(); + let mut cursor = self + .chunks() + .find(doc! { "files_id": &file.id }, options) + .await?; + + let mut n = 0; + while cursor.advance().await? { + let chunk = cursor.deserialize_current()?; + if chunk.n != n { + return Err(ErrorKind::GridFs(GridFsErrorKind::MissingChunk { n }).into()); + } + + let chunk_length = chunk.data.bytes.len(); + let expected_length = + std::cmp::min(total_bytes_expected - chunk_size * n as u64, chunk_size); + if chunk_length as u64 != expected_length { + return Err(ErrorKind::GridFs(GridFsErrorKind::WrongSizeChunk { + actual_size: chunk_length as u32, + expected_size: expected_length as u32, + }) + .into()); + } + + destination.write_all(chunk.data.bytes).await?; + n += 1; + } + + let expected_n = + total_bytes_expected / chunk_size + u64::from(total_bytes_expected % chunk_size != 0); + if (n as u64) != expected_n { + return Err(ErrorKind::GridFs(GridFsErrorKind::WrongNumberOfChunks { + actual_number: n, + expected_number: expected_n as u32, + }) + .into()); + } + + Ok(()) + } +} diff --git a/src/gridfs/options.rs b/src/gridfs/options.rs index cc4a31014..67368ddff 100644 --- a/src/gridfs/options.rs +++ b/src/gridfs/options.rs @@ -1,16 +1,16 @@ +use std::time::Duration; + use crate::{ + bson::Document, concern::{ReadConcern, WriteConcern}, selection_criteria::SelectionCriteria, }; use serde::Deserialize; -use std::time::Duration; use typed_builder::TypedBuilder; -use bson::Document; - /// Contains the options for creating a [`GridFsBucket`]. #[derive(Clone, Debug, Default, Deserialize, TypedBuilder)] -#[builder(field_defaults(setter(into)))] +#[builder(field_defaults(default, setter(into)))] #[non_exhaustive] pub struct GridFsBucketOptions { /// The bucket name. Defaults to 'fs'. @@ -32,6 +32,7 @@ pub struct GridFsBucketOptions { /// Contains the options for creating a [`GridFsUploadStream`] to upload a file to a /// [`GridFsBucket`]. #[derive(Clone, Debug, Default, Deserialize, TypedBuilder)] +#[serde(rename_all = "camelCase")] #[builder(field_defaults(setter(into)))] #[non_exhaustive] pub struct GridFsUploadOptions { diff --git a/src/test/spec/gridfs.rs b/src/test/spec/gridfs.rs new file mode 100644 index 000000000..2b07c3d7b --- /dev/null +++ b/src/test/spec/gridfs.rs @@ -0,0 +1,22 @@ +use crate::test::{ + run_spec_test_with_path, + spec::unified_runner::{run_unified_format_test_filtered, TestCase}, + LOCK, +}; + +#[cfg_attr(feature = "tokio-runtime", tokio::test)] +#[cfg_attr(feature = "async-std-runtime", async_std::test)] +async fn run() { + let _guard = LOCK.run_concurrently().await; + run_spec_test_with_path(&["gridfs"], |path, f| { + run_unified_format_test_filtered(path, f, test_predicate) + }) + .await; +} + +fn test_predicate(test: &TestCase) -> bool { + let lower = test.description.to_lowercase(); + + // The Rust driver doesn't support the disableMD5 and contentType options for upload. + !lower.contains("sans md5") && !lower.contains("contenttype") +} diff --git a/src/test/spec/json/gridfs/README.rst b/src/test/spec/json/gridfs/README.rst new file mode 100644 index 000000000..9eb0f3482 --- /dev/null +++ b/src/test/spec/json/gridfs/README.rst @@ -0,0 +1,37 @@ +============ +GridFS Tests +============ + +.. contents:: + +---- + +Introduction +============ + +The YAML and JSON files in this directory are platform-independent tests +meant to exercise a driver's implementation of GridFS. These tests utilize the +`Unified Test Format <../../unified-test-format/unified-test-format.rst>`__. + +Conventions for Expressing Binary Data +====================================== + +The unified test format allows binary stream data to be expressed and matched +with ``$$hexBytes`` (for uploads) and ``$$matchesHexBytes`` (for downloads), +respectively; however, those operators are not supported in all contexts, such +as ``insertData`` and ``outcome``. When binary data must be expressed as a +base64-encoded string (`Extended JSON <../../extended-json.rst>`__ for a BSON +binary type), the test SHOULD include a comment noting the equivalent value in +hexadecimal for human-readability. For example: + +.. code:: yaml + + data: { $binary: { base64: "ESIzRA==", subType: "00" } } # hex 11223344 + +Creating the base64-encoded string for a sequence of hexadecimal bytes is left +as an exercise to the developer. Consider the following PHP one-liner: + +.. code:: shell-session + + $ php -r 'echo base64_encode(hex2bin('11223344')), "\n";' + ESIzRA== diff --git a/src/test/spec/json/gridfs/delete.json b/src/test/spec/json/gridfs/delete.json new file mode 100644 index 000000000..7a4ec27f8 --- /dev/null +++ b/src/test/spec/json/gridfs/delete.json @@ -0,0 +1,799 @@ +{ + "description": "gridfs-delete", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "gridfs-tests" + } + }, + { + "bucket": { + "id": "bucket0", + "database": "database0" + } + }, + { + "collection": { + "id": "bucket0_files_collection", + "database": "database0", + "collectionName": "fs.files" + } + }, + { + "collection": { + "id": "bucket0_chunks_collection", + "database": "database0", + "collectionName": "fs.chunks" + } + } + ], + "initialData": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "dd254cdc958e53abaa67da9f797125f5", + "filename": "length-8", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + } + ] + } + ], + "tests": [ + { + "description": "delete when length is 0", + "operations": [ + { + "name": "delete", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000001" + } + } + } + ], + "outcome": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "dd254cdc958e53abaa67da9f797125f5", + "filename": "length-8", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "delete when length is 0 and there is one extra empty chunk", + "operations": [ + { + "name": "delete", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000002" + } + } + } + ], + "outcome": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "dd254cdc958e53abaa67da9f797125f5", + "filename": "length-8", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "delete when length is 8", + "operations": [ + { + "name": "delete", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000004" + } + } + } + ], + "outcome": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "delete when files entry does not exist", + "operations": [ + { + "name": "delete", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000000" + } + }, + "expectError": { + "isError": true + } + } + ], + "outcome": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "dd254cdc958e53abaa67da9f797125f5", + "filename": "length-8", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "delete when files entry does not exist and there are orphaned chunks", + "operations": [ + { + "name": "deleteOne", + "object": "bucket0_files_collection", + "arguments": { + "filter": { + "_id": { + "$oid": "000000000000000000000004" + } + } + }, + "expectResult": { + "deletedCount": 1 + } + }, + { + "name": "delete", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000004" + } + }, + "expectError": { + "isError": true + } + } + ], + "outcome": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/gridfs/delete.yml b/src/test/spec/json/gridfs/delete.yml new file mode 100644 index 000000000..b300cad1b --- /dev/null +++ b/src/test/spec/json/gridfs/delete.yml @@ -0,0 +1,198 @@ +description: "gridfs-delete" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name gridfs-tests + - bucket: + id: &bucket0 bucket0 + database: *database0 + - collection: + id: &bucket0_files_collection bucket0_files_collection + database: *database0 + collectionName: &bucket0_files_collectionName fs.files + - collection: + id: &bucket0_chunks_collection bucket0_chunks_collection + database: *database0 + collectionName: &bucket0_chunks_collectionName fs.chunks + +initialData: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - &file1 + _id: { "$oid": "000000000000000000000001" } + length: 0 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "d41d8cd98f00b204e9800998ecf8427e" + filename: "length-0" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - &file2 + _id: { "$oid": "000000000000000000000002" } + length: 0 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "d41d8cd98f00b204e9800998ecf8427e" + filename: "length-0-with-empty-chunk" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - &file3 + _id: { "$oid": "000000000000000000000003" } + length: 2 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "c700ed4fdb1d27055aa3faa2c2432283" + filename: "length-2" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - &file4 + _id: { "$oid": "000000000000000000000004" } + length: 8 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "dd254cdc958e53abaa67da9f797125f5" + filename: "length-8" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - &file2_chunk0 + _id: { "$oid": "000000000000000000000001" } + files_id: { "$oid": "000000000000000000000002" } + n: 0 + data: { "$binary": { "base64": "", "subType": "00" } } + - &file3_chunk0 + _id: { "$oid": "000000000000000000000002" } + files_id: { "$oid": "000000000000000000000003" } + n: 0 + data: { "$binary": { "base64": "ESI=", "subType": "00" } } # hex: 1122 + - &file4_chunk0 + _id: { "$oid": "000000000000000000000003" } + files_id: { "$oid": "000000000000000000000004" } + n: 0 + data: { "$binary": { "base64": "ESIzRA==", "subType": "00" } } # hex: 11223344 + - &file4_chunk1 + _id: { "$oid": "000000000000000000000004" } + files_id: { "$oid": "000000000000000000000004" } + n: 1 + data: { "$binary": { "base64": "VWZ3iA==", "subType": "00" } } # hex: 55667788 + +tests: + - description: "delete when length is 0" + operations: + - name: delete + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000001" } + outcome: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - *file2 + - *file3 + - *file4 + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - *file2_chunk0 + - *file3_chunk0 + - *file4_chunk0 + - *file4_chunk1 + - description: "delete when length is 0 and there is one extra empty chunk" + operations: + - name: delete + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000002" } + outcome: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - *file1 + - *file3 + - *file4 + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - *file3_chunk0 + - *file4_chunk0 + - *file4_chunk1 + - description: "delete when length is 8" + operations: + - name: delete + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000004" } + outcome: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - *file1 + - *file2 + - *file3 + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - *file2_chunk0 + - *file3_chunk0 + - description: "delete when files entry does not exist" + operations: + - name: delete + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000000" } + expectError: { isError: true } # FileNotFound + outcome: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - *file1 + - *file2 + - *file3 + - *file4 + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - *file2_chunk0 + - *file3_chunk0 + - *file4_chunk0 + - *file4_chunk1 + - description: "delete when files entry does not exist and there are orphaned chunks" + operations: + - name: deleteOne + object: *bucket0_files_collection + arguments: + filter: + _id: { $oid: "000000000000000000000004" } + expectResult: + deletedCount: 1 + - name: delete + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000004" } + expectError: { isError: true } # FileNotFound + outcome: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - *file1 + - *file2 + - *file3 + # Orphaned chunks are still deleted even if fs.files + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - *file2_chunk0 + - *file3_chunk0 diff --git a/src/test/spec/json/gridfs/download.json b/src/test/spec/json/gridfs/download.json new file mode 100644 index 000000000..48d324621 --- /dev/null +++ b/src/test/spec/json/gridfs/download.json @@ -0,0 +1,558 @@ +{ + "description": "gridfs-download", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "gridfs-tests" + } + }, + { + "bucket": { + "id": "bucket0", + "database": "database0" + } + }, + { + "collection": { + "id": "bucket0_files_collection", + "database": "database0", + "collectionName": "fs.files" + } + }, + { + "collection": { + "id": "bucket0_chunks_collection", + "database": "database0", + "collectionName": "fs.chunks" + } + } + ], + "initialData": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "filename": "length-0-with-empty-chunk", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "filename": "length-2", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "dd254cdc958e53abaa67da9f797125f5", + "filename": "length-8", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000005" + }, + "length": 10, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "57d83cd477bfb1ccd975ab33d827a92b", + "filename": "length-10", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000006" + }, + "length": 2, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "c700ed4fdb1d27055aa3faa2c2432283", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000005" + }, + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000006" + }, + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000007" + }, + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 2, + "data": { + "$binary": { + "base64": "mao=", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000008" + }, + "files_id": { + "$oid": "000000000000000000000006" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESI=", + "subType": "00" + } + } + } + ] + } + ], + "tests": [ + { + "description": "download when length is zero", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000001" + } + }, + "expectResult": { + "$$matchesHexBytes": "" + } + } + ] + }, + { + "description": "download when length is zero and there is one empty chunk", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000002" + } + }, + "expectResult": { + "$$matchesHexBytes": "" + } + } + ] + }, + { + "description": "download when there is one chunk", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000003" + } + }, + "expectResult": { + "$$matchesHexBytes": "1122" + } + } + ] + }, + { + "description": "download when there are two chunks", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000004" + } + }, + "expectResult": { + "$$matchesHexBytes": "1122334455667788" + } + } + ] + }, + { + "description": "download when there are three chunks", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000005" + } + }, + "expectResult": { + "$$matchesHexBytes": "112233445566778899aa" + } + } + ] + }, + { + "description": "download when files entry does not exist", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000000" + } + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "download when an intermediate chunk is missing", + "operations": [ + { + "name": "deleteOne", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": { + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 1 + } + }, + "expectResult": { + "deletedCount": 1 + } + }, + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000005" + } + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "download when final chunk is missing", + "operations": [ + { + "name": "deleteOne", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": { + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 2 + } + }, + "expectResult": { + "deletedCount": 1 + } + }, + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000005" + } + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "download when an intermediate chunk is the wrong size", + "operations": [ + { + "name": "bulkWrite", + "object": "bucket0_chunks_collection", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 1 + }, + "update": { + "$set": { + "data": { + "$binary": { + "base64": "VWZ3", + "subType": "00" + } + } + } + } + } + }, + { + "updateOne": { + "filter": { + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 2 + }, + "update": { + "$set": { + "data": { + "$binary": { + "base64": "iJmq", + "subType": "00" + } + } + } + } + } + } + ] + }, + "expectResult": { + "matchedCount": 2, + "modifiedCount": 2 + } + }, + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000005" + } + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "download when final chunk is the wrong size", + "operations": [ + { + "name": "updateOne", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": { + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 2 + }, + "update": { + "$set": { + "data": { + "$binary": { + "base64": "mQ==", + "subType": "00" + } + } + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1 + } + }, + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000005" + } + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "download legacy file with no name", + "operations": [ + { + "name": "download", + "object": "bucket0", + "arguments": { + "id": { + "$oid": "000000000000000000000006" + } + }, + "expectResult": { + "$$matchesHexBytes": "1122" + } + } + ] + } + ] +} diff --git a/src/test/spec/json/gridfs/download.yml b/src/test/spec/json/gridfs/download.yml new file mode 100644 index 000000000..3da5ee950 --- /dev/null +++ b/src/test/spec/json/gridfs/download.yml @@ -0,0 +1,241 @@ +description: "gridfs-download" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name gridfs-tests + - bucket: + id: &bucket0 bucket0 + database: *database0 + - collection: + id: &bucket0_files_collection bucket0_files_collection + database: *database0 + collectionName: &bucket0_files_collectionName fs.files + - collection: + id: &bucket0_chunks_collection bucket0_chunks_collection + database: *database0 + collectionName: &bucket0_chunks_collectionName fs.chunks + +initialData: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - _id: { "$oid": "000000000000000000000001" } + length: 0 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "d41d8cd98f00b204e9800998ecf8427e" + filename: "length-0" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { "$oid": "000000000000000000000002" } + length: 0 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "d41d8cd98f00b204e9800998ecf8427e" + filename: "length-0-with-empty-chunk" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { "$oid": "000000000000000000000003" } + length: 2 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "c700ed4fdb1d27055aa3faa2c2432283" + filename: "length-2" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { "$oid": "000000000000000000000004" } + length: 8 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "dd254cdc958e53abaa67da9f797125f5" + filename: "length-8" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { "$oid": "000000000000000000000005" } + length: 10 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "57d83cd477bfb1ccd975ab33d827a92b" + filename: "length-10" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { "$oid": "000000000000000000000006" } + length: 2 + chunkSize: 4 + uploadDate: { "$date": "1970-01-01T00:00:00.000Z" } + md5: "c700ed4fdb1d27055aa3faa2c2432283" + # filename is intentionally omitted + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - _id: { "$oid": "000000000000000000000001" } + files_id: { "$oid": "000000000000000000000002" } + n: 0 + data: { "$binary": { "base64": "", "subType": "00" } } + - _id: { "$oid": "000000000000000000000002" } + files_id: { "$oid": "000000000000000000000003" } + n: 0 + data: { "$binary": { "base64": "ESI=", "subType": "00" } } # hex: 1122 + - _id: { "$oid": "000000000000000000000003" } + files_id: { "$oid": "000000000000000000000004" } + n: 0 + data: { "$binary": { "base64": "ESIzRA==", "subType": "00" } } # hex: 11223344 + - _id: { "$oid": "000000000000000000000004" } + files_id: { "$oid": "000000000000000000000004" } + n: 1 + data: { "$binary": { "base64": "VWZ3iA==", "subType": "00" } } # hex: 55667788 + - _id: { "$oid": "000000000000000000000005" } + files_id: { "$oid": "000000000000000000000005" } + n: 0 + data: { "$binary": { "base64": "ESIzRA==", "subType": "00" } } # hex: 11223344 + - _id: { "$oid": "000000000000000000000006" } + files_id: { "$oid": "000000000000000000000005" } + n: 1 + data: { "$binary": { "base64": "VWZ3iA==", "subType": "00" } } # hex: 55667788 + - _id: { "$oid": "000000000000000000000007" } + files_id: { "$oid": "000000000000000000000005" } + n: 2 + data: { "$binary" : { "base64": "mao=", "subType" : "00" } } # hex: 99aa + - _id: { "$oid": "000000000000000000000008" } + files_id: { "$oid": "000000000000000000000006" } + n: 0 + data: { "$binary": { "base64": "ESI=", "subType": "00" } } # hex: 1122 + +tests: + - description: "download when length is zero" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000001" } + expectResult: { $$matchesHexBytes: "" } + - description: "download when length is zero and there is one empty chunk" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000002" } + expectResult: { $$matchesHexBytes: "" } + - description: "download when there is one chunk" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000003" } + expectResult: { $$matchesHexBytes: "1122" } + - description: "download when there are two chunks" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000004" } + expectResult: { $$matchesHexBytes: "1122334455667788" } + - description: "download when there are three chunks" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000005" } + expectResult: { $$matchesHexBytes: "112233445566778899aa" } + - description: "download when files entry does not exist" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000000" } + expectError: { isError: true } # FileNotFound + - description: "download when an intermediate chunk is missing" + operations: + - name: deleteOne + object: *bucket0_chunks_collection + arguments: + filter: + files_id: { $oid: "000000000000000000000005" } + n: 1 + expectResult: + deletedCount: 1 + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000005" } + expectError: { isError: true } # ChunkIsMissing + - description: "download when final chunk is missing" + operations: + - name: deleteOne + object: *bucket0_chunks_collection + arguments: + filter: + files_id: { $oid: "000000000000000000000005" } + n: 2 + expectResult: + deletedCount: 1 + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000005" } + expectError: { isError: true } # ChunkIsMissing + - description: "download when an intermediate chunk is the wrong size" + operations: + - name: bulkWrite + object: *bucket0_chunks_collection + arguments: + requests: + - updateOne: + filter: + files_id: { $oid: "000000000000000000000005" } + n: 1 + update: + $set: { data: { "$binary": { "base64": "VWZ3", "subType": "00" } } } # hex: 556677 + - updateOne: + filter: + files_id: { $oid: "000000000000000000000005" } + n: 2 + update: + $set: { data: { "$binary": { "base64": "iJmq", "subType": "00" } } } # hex: 8899aa + expectResult: + matchedCount: 2 + modifiedCount: 2 + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000005" } + expectError: { isError: true } # ChunkIsWrongSize + - description: "download when final chunk is the wrong size" + operations: + - name: updateOne + object: *bucket0_chunks_collection + arguments: + filter: + files_id: { $oid: "000000000000000000000005" } + n: 2 + update: + $set: { data: { "$binary": { "base64": "mQ==", "subType": "00" } } } # hex: 99 + expectResult: + matchedCount: 1 + modifiedCount: 1 + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000005" } + expectError: { isError: true } # ChunkIsWrongSize + - description: "download legacy file with no name" + operations: + - name: download + object: *bucket0 + arguments: + id: { $oid: "000000000000000000000006" } + expectResult: { $$matchesHexBytes: "1122" } diff --git a/src/test/spec/json/gridfs/downloadByName.json b/src/test/spec/json/gridfs/downloadByName.json new file mode 100644 index 000000000..cd4466395 --- /dev/null +++ b/src/test/spec/json/gridfs/downloadByName.json @@ -0,0 +1,330 @@ +{ + "description": "gridfs-downloadByName", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "gridfs-tests" + } + }, + { + "bucket": { + "id": "bucket0", + "database": "database0" + } + }, + { + "collection": { + "id": "bucket0_files_collection", + "database": "database0", + "collectionName": "fs.files" + } + }, + { + "collection": { + "id": "bucket0_chunks_collection", + "database": "database0", + "collectionName": "fs.chunks" + } + } + ], + "initialData": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "47ed733b8d10be225eceba344d533586", + "filename": "abc", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-02T00:00:00.000Z" + }, + "md5": "b15835f133ff2e27c7cb28117bfae8f4", + "filename": "abc", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-03T00:00:00.000Z" + }, + "md5": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "filename": "abc", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-04T00:00:00.000Z" + }, + "md5": "f623e75af30e62bbd73d6df5b50bb7b5", + "filename": "abc", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + }, + { + "_id": { + "$oid": "000000000000000000000005" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-05T00:00:00.000Z" + }, + "md5": "4c614360da93c0a041b22e537de151eb", + "filename": "abc", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [ + { + "_id": { + "$oid": "000000000000000000000001" + }, + "files_id": { + "$oid": "000000000000000000000001" + }, + "n": 0, + "data": { + "$binary": { + "base64": "EQ==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000002" + }, + "files_id": { + "$oid": "000000000000000000000002" + }, + "n": 0, + "data": { + "$binary": { + "base64": "Ig==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000003" + }, + "files_id": { + "$oid": "000000000000000000000003" + }, + "n": 0, + "data": { + "$binary": { + "base64": "Mw==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000004" + }, + "files_id": { + "$oid": "000000000000000000000004" + }, + "n": 0, + "data": { + "$binary": { + "base64": "RA==", + "subType": "00" + } + } + }, + { + "_id": { + "$oid": "000000000000000000000005" + }, + "files_id": { + "$oid": "000000000000000000000005" + }, + "n": 0, + "data": { + "$binary": { + "base64": "VQ==", + "subType": "00" + } + } + } + ] + } + ], + "tests": [ + { + "description": "downloadByName defaults to latest revision (-1)", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc" + }, + "expectResult": { + "$$matchesHexBytes": "55" + } + } + ] + }, + { + "description": "downloadByName when revision is 0", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": 0 + }, + "expectResult": { + "$$matchesHexBytes": "11" + } + } + ] + }, + { + "description": "downloadByName when revision is 1", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": 1 + }, + "expectResult": { + "$$matchesHexBytes": "22" + } + } + ] + }, + { + "description": "downloadByName when revision is 2", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": 2 + }, + "expectResult": { + "$$matchesHexBytes": "33" + } + } + ] + }, + { + "description": "downloadByName when revision is -2", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": -2 + }, + "expectResult": { + "$$matchesHexBytes": "44" + } + } + ] + }, + { + "description": "downloadByName when revision is -1", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": -1 + }, + "expectResult": { + "$$matchesHexBytes": "55" + } + } + ] + }, + { + "description": "downloadByName when files entry does not exist", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "xyz" + }, + "expectError": { + "isError": true + } + } + ] + }, + { + "description": "downloadByName when revision does not exist", + "operations": [ + { + "name": "downloadByName", + "object": "bucket0", + "arguments": { + "filename": "abc", + "revision": 999 + }, + "expectError": { + "isError": true + } + } + ] + } + ] +} diff --git a/src/test/spec/json/gridfs/downloadByName.yml b/src/test/spec/json/gridfs/downloadByName.yml new file mode 100644 index 000000000..6dfc602b6 --- /dev/null +++ b/src/test/spec/json/gridfs/downloadByName.yml @@ -0,0 +1,159 @@ +description: "gridfs-downloadByName" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name gridfs-tests + - bucket: + id: &bucket0 bucket0 + database: *database0 + - collection: + id: &bucket0_files_collection bucket0_files_collection + database: *database0 + collectionName: &bucket0_files_collectionName fs.files + - collection: + id: &bucket0_chunks_collection bucket0_chunks_collection + database: *database0 + collectionName: &bucket0_chunks_collectionName fs.chunks + +initialData: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: + - _id: { $oid: "000000000000000000000001" } + length: 1 + chunkSize: 4 + uploadDate: { $date: "1970-01-01T00:00:00.000Z" } + md5: "47ed733b8d10be225eceba344d533586" + filename: "abc" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { $oid: "000000000000000000000002" } + length: 1 + chunkSize: 4 + uploadDate: { $date: "1970-01-02T00:00:00.000Z" } + md5: "b15835f133ff2e27c7cb28117bfae8f4" + filename: "abc" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { $oid: "000000000000000000000003" } + length: 1 + chunkSize: 4 + uploadDate: { $date: "1970-01-03T00:00:00.000Z" } + md5: "eccbc87e4b5ce2fe28308fd9f2a7baf3" + filename: "abc" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { $oid: "000000000000000000000004" } + length: 1 + chunkSize: 4 + uploadDate: { $date: "1970-01-04T00:00:00.000Z" } + md5: "f623e75af30e62bbd73d6df5b50bb7b5" + filename: "abc" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - _id: { $oid: "000000000000000000000005" } + length: 1 + chunkSize: 4 + uploadDate: { $date: "1970-01-05T00:00:00.000Z" } + md5: "4c614360da93c0a041b22e537de151eb" + filename: "abc" + contentType: "application/octet-stream" + aliases: [] + metadata: {} + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: + - _id: { $oid: "000000000000000000000001" } + files_id: { $oid: "000000000000000000000001" } + n: 0 + data: { "$binary": { "base64": "EQ==", "subType": "00" } } # hex: 11 + - _id: { $oid: "000000000000000000000002" } + files_id: { $oid: "000000000000000000000002" } + n: 0 + data: { "$binary": { "base64": "Ig==", "subType": "00" } } # hex: 22 + - _id: { $oid: "000000000000000000000003" } + files_id: { $oid: "000000000000000000000003" } + n: 0 + data: { "$binary": { "base64": "Mw==", "subType": "00" } } # hex: 33 + - _id: { $oid: "000000000000000000000004" } + files_id: { $oid: "000000000000000000000004" } + n: 0 + data: { "$binary": { "base64": "RA==", "subType": "00" } } # hex: 44 + - _id: { $oid: "000000000000000000000005" } + files_id: { $oid: "000000000000000000000005" } + n: 0 + data: { "$binary": { "base64": "VQ==", "subType": "00" } } # hex: 55 + +tests: + - description: "downloadByName defaults to latest revision (-1)" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + expectResult: { $$matchesHexBytes: "55" } + - description: "downloadByName when revision is 0" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: 0 + expectResult: { $$matchesHexBytes: "11" } + - description: "downloadByName when revision is 1" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: 1 + expectResult: { $$matchesHexBytes: "22" } + - description: "downloadByName when revision is 2" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: 2 + expectResult: { $$matchesHexBytes: "33" } + - description: "downloadByName when revision is -2" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: -2 + expectResult: { $$matchesHexBytes: "44" } + - description: "downloadByName when revision is -1" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: -1 + expectResult: { $$matchesHexBytes: "55" } + - description: "downloadByName when files entry does not exist" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "xyz" + expectError: { isError: true } # FileNotFound + - description: "downloadByName when revision does not exist" + operations: + - name: downloadByName + object: *bucket0 + arguments: + filename: "abc" + revision: 999 + expectError: { isError: true } # RevisionNotFound diff --git a/src/test/spec/json/gridfs/upload-disableMD5.json b/src/test/spec/json/gridfs/upload-disableMD5.json new file mode 100644 index 000000000..d5a9d6f4a --- /dev/null +++ b/src/test/spec/json/gridfs/upload-disableMD5.json @@ -0,0 +1,172 @@ +{ + "description": "gridfs-upload-disableMD5", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "gridfs-tests" + } + }, + { + "bucket": { + "id": "bucket0", + "database": "database0" + } + }, + { + "collection": { + "id": "bucket0_files_collection", + "database": "database0", + "collectionName": "fs.files" + } + }, + { + "collection": { + "id": "bucket0_chunks_collection", + "database": "database0", + "collectionName": "fs.chunks" + } + } + ], + "initialData": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "upload when length is 0 sans MD5", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "" + }, + "chunkSizeBytes": 4, + "disableMD5": true + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$exists": false + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ] + }, + { + "description": "upload when length is 1 sans MD5", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "11" + }, + "chunkSizeBytes": 4, + "disableMD5": true + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$exists": false + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "EQ==", + "subType": "00" + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/gridfs/upload-disableMD5.yml b/src/test/spec/json/gridfs/upload-disableMD5.yml new file mode 100644 index 000000000..4f77f5a1a --- /dev/null +++ b/src/test/spec/json/gridfs/upload-disableMD5.yml @@ -0,0 +1,92 @@ +description: "gridfs-upload-disableMD5" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name gridfs-tests + - bucket: + id: &bucket0 bucket0 + database: *database0 + - collection: + id: &bucket0_files_collection bucket0_files_collection + database: *database0 + collectionName: &bucket0_files_collectionName fs.files + - collection: + id: &bucket0_chunks_collection bucket0_chunks_collection + database: *database0 + collectionName: &bucket0_chunks_collectionName fs.chunks + +initialData: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: [] + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: [] + +# Note: these tests utilize the transitional "disableMD5" option. Drivers that +# do not support the option should skip this file. +tests: + - description: "upload when length is 0 sans MD5" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "" } + chunkSizeBytes: 4 + disableMD5: true + expectResult: { $$type: objectId } + saveResultAsEntity: &uploadedObjectId uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 0 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$exists: false } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: [] + - description: "upload when length is 1 sans MD5" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "11" } + chunkSizeBytes: 4 + disableMD5: true + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 1 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$exists: false } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "EQ==", subType: "00" } } # hex 11 diff --git a/src/test/spec/json/gridfs/upload.json b/src/test/spec/json/gridfs/upload.json new file mode 100644 index 000000000..97e18d2bc --- /dev/null +++ b/src/test/spec/json/gridfs/upload.json @@ -0,0 +1,616 @@ +{ + "description": "gridfs-upload", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "gridfs-tests" + } + }, + { + "bucket": { + "id": "bucket0", + "database": "database0" + } + }, + { + "collection": { + "id": "bucket0_files_collection", + "database": "database0", + "collectionName": "fs.files" + } + }, + { + "collection": { + "id": "bucket0_chunks_collection", + "database": "database0", + "collectionName": "fs.chunks" + } + } + ], + "initialData": [ + { + "collectionName": "fs.files", + "databaseName": "gridfs-tests", + "documents": [] + }, + { + "collectionName": "fs.chunks", + "databaseName": "gridfs-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "upload when length is 0", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 0, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "d41d8cd98f00b204e9800998ecf8427e" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ] + }, + { + "description": "upload when length is 1", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "11" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "47ed733b8d10be225eceba344d533586" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "EQ==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when length is 3", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "112233" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 3, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "bafae3a174ab91fc70db7a6aa50f4f52" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIz", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when length is 4", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "11223344" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 4, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "7e7c77cff5705d1f7574a25ef6662117" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when length is 5", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "1122334455" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 5, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "283d4fea5dded59cf837d3047328f5af" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {}, + "sort": { + "n": 1 + } + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VQ==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when length is 8", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "1122334455667788" + }, + "chunkSizeBytes": 4 + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 8, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "dd254cdc958e53abaa67da9f797125f5" + }, + "filename": "filename" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {}, + "sort": { + "n": 1 + } + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "ESIzRA==", + "subType": "00" + } + } + }, + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 1, + "data": { + "$binary": { + "base64": "VWZ3iA==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when contentType is provided", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "11" + }, + "chunkSizeBytes": 4, + "contentType": "image/jpeg" + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "47ed733b8d10be225eceba344d533586" + }, + "filename": "filename", + "contentType": "image/jpeg" + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "EQ==", + "subType": "00" + } + } + } + ] + } + ] + }, + { + "description": "upload when metadata is provided", + "operations": [ + { + "name": "upload", + "object": "bucket0", + "arguments": { + "filename": "filename", + "source": { + "$$hexBytes": "11" + }, + "chunkSizeBytes": 4, + "metadata": { + "x": 1 + } + }, + "expectResult": { + "$$type": "objectId" + }, + "saveResultAsEntity": "uploadedObjectId" + }, + { + "name": "find", + "object": "bucket0_files_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "length": 1, + "chunkSize": 4, + "uploadDate": { + "$$type": "date" + }, + "md5": { + "$$unsetOrMatches": "47ed733b8d10be225eceba344d533586" + }, + "filename": "filename", + "metadata": { + "x": 1 + } + } + ] + }, + { + "name": "find", + "object": "bucket0_chunks_collection", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": { + "$$type": "objectId" + }, + "files_id": { + "$$matchesEntity": "uploadedObjectId" + }, + "n": 0, + "data": { + "$binary": { + "base64": "EQ==", + "subType": "00" + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/gridfs/upload.yml b/src/test/spec/json/gridfs/upload.yml new file mode 100644 index 000000000..27f3186fc --- /dev/null +++ b/src/test/spec/json/gridfs/upload.yml @@ -0,0 +1,288 @@ +description: "gridfs-upload" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name gridfs-tests + - bucket: + id: &bucket0 bucket0 + database: *database0 + - collection: + id: &bucket0_files_collection bucket0_files_collection + database: *database0 + collectionName: &bucket0_files_collectionName fs.files + - collection: + id: &bucket0_chunks_collection bucket0_chunks_collection + database: *database0 + collectionName: &bucket0_chunks_collectionName fs.chunks + +initialData: + - collectionName: *bucket0_files_collectionName + databaseName: *database0Name + documents: [] + - collectionName: *bucket0_chunks_collectionName + databaseName: *database0Name + documents: [] + +# Note: Uploaded files and chunks include ObjectIds, which we cannot match with +# "outcome" since it does not allow operators. Instead, these tests will use +# find operations to assert the contents of uploaded files and chunks. +tests: + - description: "upload when length is 0" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: &uploadedObjectId uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 0 + chunkSize: 4 + uploadDate: { $$type: date } + # The md5 field is deprecated so some drivers do not calculate it when uploading files. + md5: { $$unsetOrMatches: "d41d8cd98f00b204e9800998ecf8427e" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: [] + - description: "upload when length is 1" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "11" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 1 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "47ed733b8d10be225eceba344d533586" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "EQ==", subType: "00" } } # hex 11 + - description: "upload when length is 3" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "112233" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 3 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "bafae3a174ab91fc70db7a6aa50f4f52" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "ESIz", subType: "00" } } # hex 112233 + - description: "upload when length is 4" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "11223344" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 4 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "7e7c77cff5705d1f7574a25ef6662117" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "ESIzRA==", subType: "00" } } # hex 11223344 + - description: "upload when length is 5" + operations: + - name: upload + object: *bucket0 + arguments: + filename: filename + source: { $$hexBytes: "1122334455" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 5 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "283d4fea5dded59cf837d3047328f5af" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + # Sort to ensure chunks are returned in a deterministic order + sort: { n: 1 } + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "ESIzRA==", subType: "00" } } # hex 11223344 + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 1 + data: { $binary: { base64: "VQ==", subType: "00" } } # hex 55 + - description: "upload when length is 8" + operations: + - name: upload + object: *bucket0 + arguments: + filename: filename + source: { $$hexBytes: "1122334455667788" } + chunkSizeBytes: 4 + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 8 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "dd254cdc958e53abaa67da9f797125f5" } + filename: filename + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + # Sort to ensure chunks are returned in a deterministic order + sort: { n: 1 } + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "ESIzRA==", subType: "00" } } # hex 11223344 + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 1 + data: { $binary: { base64: "VWZ3iA==", subType: "00" } } # hex 55667788 + - description: "upload when contentType is provided" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "11" } + chunkSizeBytes: 4 + contentType: "image/jpeg" + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 1 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "47ed733b8d10be225eceba344d533586" } + filename: filename + contentType: "image/jpeg" + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "EQ==", subType: "00" } } # hex 11 + - description: "upload when metadata is provided" + operations: + - name: upload + object: *bucket0 + arguments: + filename: "filename" + source: { $$hexBytes: "11" } + chunkSizeBytes: 4 + metadata: { x: 1 } + expectResult: { $$type: objectId } + saveResultAsEntity: *uploadedObjectId + - name: find + object: *bucket0_files_collection + arguments: + filter: {} + expectResult: + - _id: { $$matchesEntity: *uploadedObjectId } + length: 1 + chunkSize: 4 + uploadDate: { $$type: date } + md5: { $$unsetOrMatches: "47ed733b8d10be225eceba344d533586" } + filename: filename + metadata: { x: 1 } + - name: find + object: *bucket0_chunks_collection + arguments: + filter: {} + expectResult: + - _id: { $$type: objectId } + files_id: { $$matchesEntity: *uploadedObjectId } + n: 0 + data: { $binary: { base64: "EQ==", subType: "00" } } # hex 11 diff --git a/src/test/spec/mod.rs b/src/test/spec/mod.rs index d68576571..b4f0f7ff6 100644 --- a/src/test/spec/mod.rs +++ b/src/test/spec/mod.rs @@ -6,6 +6,7 @@ mod command_monitoring; mod connection_stepdown; mod crud; mod crud_v1; +mod gridfs; #[cfg(all(not(feature = "sync"), not(feature = "tokio-sync")))] mod initial_dns_seedlist_discovery; mod load_balancers; diff --git a/src/test/spec/unified_runner/entity.rs b/src/test/spec/unified_runner/entity.rs index c8f752b44..fc2dff7e5 100644 --- a/src/test/spec/unified_runner/entity.rs +++ b/src/test/spec/unified_runner/entity.rs @@ -14,6 +14,7 @@ use crate::{ client::{HELLO_COMMAND_NAMES, REDACTED_COMMANDS}, error::{Error, Result}, event::command::CommandStartedEvent, + gridfs::GridFsBucket, runtime, sdam::TopologyDescription, test::{ @@ -39,6 +40,7 @@ pub(crate) enum Entity { Database(Database), Collection(Collection), Session(SessionEntity), + Bucket(GridFsBucket), Cursor(TestCursor), Bson(Bson), EventList(EventList), @@ -256,6 +258,12 @@ impl From for Entity { } } +impl From for Entity { + fn from(bucket: GridFsBucket) -> Self { + Self::Bucket(bucket) + } +} + impl Deref for ClientEntity { type Target = Client; @@ -320,7 +328,14 @@ impl Entity { } } - pub(crate) fn as_mut_session_entity(&mut self) -> &mut SessionEntity { + pub fn as_bucket_entity(&self) -> &GridFsBucket { + match self { + Self::Bucket(gridfs_bucket) => gridfs_bucket, + _ => panic!("Expected bucket entity, got {:?}", &self), + } + } + + pub fn as_mut_session_entity(&mut self) -> &mut SessionEntity { match self { Self::Session(client_session) => client_session, _ => panic!("Expected mutable client session entity, got {:?}", &self), diff --git a/src/test/spec/unified_runner/matcher.rs b/src/test/spec/unified_runner/matcher.rs index 70f07a712..d3b92e021 100644 --- a/src/test/spec/unified_runner/matcher.rs +++ b/src/test/spec/unified_runner/matcher.rs @@ -315,7 +315,23 @@ fn special_operator_matches( let id = value.as_str().unwrap(); entity_matches(id, actual, entities.unwrap()) } - "$$matchesHexBytes" => panic!("GridFS not implemented"), + "$$matchesHexBytes" => match (actual, value) { + (Some(Bson::String(actual)), Bson::String(expected)) => { + if actual.to_lowercase() == expected.to_lowercase() { + Ok(()) + } else { + Err(format!( + "hex bytes do not match: expected {:?} but got {:?}", + actual, expected + )) + } + } + (actual, expected) => Err(format!( + "actual and expected should both be BSON strings but got: actual {:?}, expected \ + {:?}", + actual, expected + )), + }, "$$sessionLsid" => match entities { Some(entity_map) => { let session_id = value.as_str().unwrap(); diff --git a/src/test/spec/unified_runner/operation.rs b/src/test/spec/unified_runner/operation.rs index 80e0fd486..c4a80c0b5 100644 --- a/src/test/spec/unified_runner/operation.rs +++ b/src/test/spec/unified_runner/operation.rs @@ -38,6 +38,7 @@ use crate::{ coll::options::Hint, collation::Collation, error::{ErrorKind, Result}, + gridfs::options::GridFsDownloadByNameOptions, options::{ AggregateOptions, CountOptions, @@ -338,6 +339,8 @@ impl<'de> Deserialize<'de> for Operation { "waitForPrimaryChange" => deserialize_op::(definition.arguments), "wait" => deserialize_op::(definition.arguments), "createEntities" => deserialize_op::(definition.arguments), + "download" => deserialize_op::(definition.arguments), + "downloadByName" => deserialize_op::(definition.arguments), _ => Ok(Box::new(UnimplementedOperation) as Box), } .map_err(|e| serde::de::Error::custom(format!("{}", e)))?; @@ -2640,6 +2643,62 @@ impl TestOperation for CreateEntities { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(super) struct Download { + id: Bson, +} + +impl TestOperation for Download { + fn execute_entity_operation<'a>( + &'a self, + id: &'a str, + test_runner: &'a TestRunner, + ) -> BoxFuture<'a, Result>> { + async move { + let bucket = test_runner.get_bucket(id).await; + let mut buf: Vec = vec![]; + bucket + .download_to_futures_0_3_writer(self.id.clone(), &mut buf) + .await?; + let data = hex::encode(buf); + Ok(Some(Entity::Bson(data.into()))) + } + .boxed() + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(super) struct DownloadByName { + filename: String, + #[serde(flatten)] + options: GridFsDownloadByNameOptions, +} + +impl TestOperation for DownloadByName { + fn execute_entity_operation<'a>( + &'a self, + id: &'a str, + test_runner: &'a TestRunner, + ) -> BoxFuture<'a, Result>> { + async move { + let bucket = test_runner.get_bucket(id).await; + let mut buf: Vec = vec![]; + bucket + .download_to_futures_0_3_writer_by_name( + self.filename.clone(), + &mut buf, + self.options.clone(), + ) + .await?; + let data = hex::encode(buf); + Ok(Some(Entity::Bson(data.into()))) + } + .boxed() + } +} + #[derive(Debug, Deserialize)] pub(super) struct UnimplementedOperation; diff --git a/src/test/spec/unified_runner/test_file.rs b/src/test/spec/unified_runner/test_file.rs index 84804a465..7586b1de1 100644 --- a/src/test/spec/unified_runner/test_file.rs +++ b/src/test/spec/unified_runner/test_file.rs @@ -13,6 +13,7 @@ use crate::{ client::options::{ServerApi, ServerApiVersion, SessionOptions}, concern::{Acknowledgment, ReadConcernLevel}, error::Error, + gridfs::options::GridFsBucketOptions, options::{ ClientOptions, CollectionOptions, @@ -287,14 +288,12 @@ pub(crate) struct Session { pub(crate) session_options: Option, } -// TODO: RUST-527 remove the unused annotation #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[allow(unused)] pub(crate) struct Bucket { pub(crate) id: String, pub(crate) database: String, - pub(crate) bucket_options: Option, + pub(crate) bucket_options: Option, } #[derive(Debug, Deserialize)] diff --git a/src/test/spec/unified_runner/test_runner.rs b/src/test/spec/unified_runner/test_runner.rs index 6f137ac16..94cd3068d 100644 --- a/src/test/spec/unified_runner/test_runner.rs +++ b/src/test/spec/unified_runner/test_runner.rs @@ -8,6 +8,7 @@ use crate::{ bson::{doc, Document}, client::options::ClientOptions, concern::{Acknowledgment, WriteConcern}, + gridfs::GridFsBucket, options::{ CollectionOptions, CreateCollectionOptions, @@ -56,11 +57,11 @@ use super::{ const SKIPPED_OPERATIONS: &[&str] = &[ "bulkWrite", "count", - "download", - "download_by_name", + "delete", "listCollectionObjects", "listDatabaseObjects", "mapReduce", + "upload", "watch", ]; @@ -128,6 +129,16 @@ impl TestRunner { )); for test_case in test_file.tests { + if let Ok(description) = std::env::var("TEST_DESCRIPTION") { + if !test_case + .description + .to_lowercase() + .contains(&description.to_lowercase()) + { + continue; + } + } + if let Some(skip_reason) = test_case.skip_reason { log_uncaptured(format!( "Skipping test case {:?}: {}", @@ -435,8 +446,13 @@ impl TestRunner { .unwrap(); (id, Entity::Session(SessionEntity::new(client_session))) } - TestFileEntity::Bucket(_) => { - panic!("GridFS not implemented"); + TestFileEntity::Bucket(bucket) => { + let id = bucket.id.clone(); + let database = self.get_database(&bucket.database).await; + ( + id, + Entity::Bucket(database.gridfs_bucket(bucket.bucket_options.clone())), + ) } TestFileEntity::Thread(thread) => { let (sender, mut receiver) = mpsc::unbounded_channel::(); @@ -520,6 +536,16 @@ impl TestRunner { .clone() } + pub(crate) async fn get_bucket(&self, id: &str) -> GridFsBucket { + self.entities + .read() + .await + .get(id) + .unwrap() + .as_bucket_entity() + .clone() + } + pub(crate) async fn get_thread(&self, id: &str) -> ThreadEntity { self.entities .read()