diff --git a/src/bson_util.rs b/src/bson_util.rs index d15d2b670..c61e0499a 100644 --- a/src/bson_util.rs +++ b/src/bson_util.rs @@ -34,6 +34,19 @@ pub(crate) fn get_int(val: &Bson) -> Option { } } +/// Coerce numeric types into an `f64` if it would be lossless to do so. If this Bson is not numeric +/// or the conversion would be lossy (e.g. 1.5 -> 1), this returns `None`. +#[cfg(test)] +#[allow(clippy::cast_possible_truncation)] +pub(crate) fn get_double(val: &Bson) -> Option { + match *val { + Bson::Int32(i) => Some(f64::from(i)), + Bson::Int64(i) if i == i as f64 as i64 => Some(i as f64), + Bson::Double(f) => Some(f), + _ => None, + } +} + /// Coerce numeric types into an `i64` if it would be lossless to do so. If this Bson is not numeric /// or the conversion would be lossy (e.g. 1.5 -> 1), this returns `None`. pub(crate) fn get_int_raw(val: RawBsonRef<'_>) -> Option { diff --git a/src/test/spec/json/unified-test-format/Makefile b/src/test/spec/json/unified-test-format/Makefile index b21d6e426..1a049e72c 100644 --- a/src/test/spec/json/unified-test-format/Makefile +++ b/src/test/spec/json/unified-test-format/Makefile @@ -1,8 +1,8 @@ -SCHEMA=../schema-1.13.json +SCHEMA=../schema-1.23.json -.PHONY: all invalid valid-fail valid-pass versioned-api load-balancers gridfs transactions crud collection-management sessions command-logging-and-monitoring client-side-operations-timeout HAS_AJV +.PHONY: all invalid valid-fail valid-pass atlas-data-lake versioned-api load-balancers gridfs transactions transactions-convenient-api crud collection-management read-write-concern retryable-reads retryable-writes sessions command-logging-and-monitoring client-side-operations-timeout HAS_AJV -all: invalid valid-fail valid-pass versioned-api load-balancers gridfs transactions change-streams crud collection-management sessions command-logging-and-monitoring client-side-operations-timeout +all: invalid valid-fail valid-pass atlas-data-lake versioned-api load-balancers gridfs transactions transactions-convenient-api change-streams crud collection-management read-write-concern retryable-reads retryable-writes sessions command-logging-and-monitoring client-side-operations-timeout client-side-encryption invalid: HAS_AJV @# Redirect stdout to hide expected validation errors @@ -14,6 +14,9 @@ valid-fail: HAS_AJV valid-pass: HAS_AJV @ajv test -s $(SCHEMA) -d "valid-pass/*.yml" --valid +atlas-data-lake: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../atlas-data-lake-testing/tests/unified/*.yml" --valid + versioned-api: HAS_AJV @ajv test -s $(SCHEMA) -d "../../versioned-api/tests/*.yml" --valid @@ -26,6 +29,9 @@ gridfs: HAS_AJV transactions: HAS_AJV @ajv test -s $(SCHEMA) -d "../../transactions/tests/unified/*.yml" --valid +transactions-convenient-api: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../transactions-convenient-api/tests/unified/*.yml" --valid + change-streams: HAS_AJV @ajv test -s $(SCHEMA) -d "../../change-streams/tests/unified/*.yml" --valid @@ -38,6 +44,15 @@ crud: HAS_AJV collection-management: HAS_AJV @ajv test -s $(SCHEMA) -d "../../collection-management/tests/*.yml" --valid +read-write-concern: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../read-write-concern/tests/operation/*.yml" --valid + +retryable-reads: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../retryable-reads/tests/unified/*.yml" --valid + +retryable-writes: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../retryable-writes/tests/unified/*.yml" --valid + sessions: HAS_AJV @ajv test -s $(SCHEMA) -d "../../sessions/tests/*.yml" --valid @@ -45,6 +60,9 @@ command-logging-and-monitoring: HAS_AJV @ajv test -s $(SCHEMA) -d "../../command-logging-and-monitoring/tests/logging/*.yml" --valid @ajv test -s $(SCHEMA) -d "../../command-logging-and-monitoring/tests/monitoring/*.yml" --valid +client-side-encryption: HAS_AJV + @ajv test -s $(SCHEMA) -d "../../client-side-encryption/tests/unified/*.yml" --valid + HAS_AJV: @if ! command -v ajv > /dev/null; then \ echo 'Error: need "npm install -g ajv-cli"' 1>&2; \ diff --git a/src/test/spec/json/unified-test-format/README.md b/src/test/spec/json/unified-test-format/README.md new file mode 100644 index 000000000..53e8180e4 --- /dev/null +++ b/src/test/spec/json/unified-test-format/README.md @@ -0,0 +1,32 @@ +# Unified Test Format Tests + +______________________________________________________________________ + +## Introduction + +This directory contains tests for the Unified Test Format's schema and test runner implementation(s). Tests are +organized in the following directories: + +- `invalid`: These files do not validate against the schema and are used to test the schema itself. +- `valid-pass`: These files validate against the schema and should pass when executed with a test runner. +- `valid-fail`: These files validate against the schema but should produce runtime errors or failures when executed with + a test runner. Some do so by violating the "SHOULD" and "SHOULD NOT" guidance in the spec (e.g. referencing an + undefined entity). + +## Validating Test Files + +JSON and YAML test files can be validated using [Ajv](https://ajv.js.org/) and a schema from the parent directory (e.g. +`schema-1.0.json`). + +Test files can be validated individually like so: + +```bash +ajv -s ../schema-1.0.json -d path/to/test.yml +``` + +Ajv can also be used to assert the validity of test files: + +```bash +ajv test -s ../schema-1.0.json -d "invalid/*.yml" --invalid +ajv test -s ../schema-1.0.json -d "valid-*/*.yml" --valid +``` diff --git a/src/test/spec/json/unified-test-format/README.rst b/src/test/spec/json/unified-test-format/README.rst deleted file mode 100644 index dd422f7f0..000000000 --- a/src/test/spec/json/unified-test-format/README.rst +++ /dev/null @@ -1,39 +0,0 @@ -========================= -Unified Test Format Tests -========================= - -.. contents:: - ----- - -Introduction -============ - -This directory contains tests for the Unified Test Format's schema and test -runner implementation(s). Tests are organized in the following directories: - -- ``invalid``: These files do not validate against the schema and are used to - test the schema itself. - -- ``valid-pass``: These files validate against the schema and should pass when - executed with a test runner. - -- ``valid-fail``: These files validate against the schema but should produce - runtime errors or failures when executed with a test runner. Some do so by - violating the "SHOULD" and "SHOULD NOT" guidance in the spec (e.g. referencing - an undefined entity). - -Validating Test Files -===================== - -JSON and YAML test files can be validated using `Ajv `__ -and a schema from the parent directory (e.g. ``schema-1.0.json``). - -Test files can be validated individually like so:: - - ajv -s ../schema-1.0.json -d path/to/test.yml - -Ajv can also be used to assert the validity of test files:: - - ajv test -s ../schema-1.0.json -d "invalid/*.yml" --invalid - ajv test -s ../schema-1.0.json -d "valid-*/*.yml" --valid diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.json b/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.json new file mode 100644 index 000000000..72b74b4a9 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.json @@ -0,0 +1,278 @@ +{ + "description": "entity-commandCursor", + "schemaVersion": "1.3", + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "runCursorCommand creates and exhausts cursor by running getMores", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "createCommandCursor creates a cursor and stores it as an entity that can be iterated one document at a time", + "operations": [ + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 2, + "x": 22 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 3, + "x": 33 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 4, + "x": 44 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 5, + "x": 55 + } + } + ] + }, + { + "description": "createCommandCursor's cursor can be closed and will perform a killCursors operation", + "operations": [ + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "close", + "object": "myRunCommandCursor" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "killCursors": "collection", + "cursors": { + "$$type": "array" + } + }, + "commandName": "killCursors" + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.yml b/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.yml new file mode 100644 index 000000000..3becf2095 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-commandCursor.yml @@ -0,0 +1,115 @@ +description: entity-commandCursor +schemaVersion: '1.3' +createEntities: + - client: + id: &client client + useMultipleMongoses: false + observeEvents: [commandStartedEvent] + - database: + id: &db db + client: *client + databaseName: *db + - collection: + id: &collection collection + database: *db + collectionName: *collection +initialData: + - collectionName: collection + databaseName: *db + documents: &documents + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } +tests: + - description: runCursorCommand creates and exhausts cursor by running getMores + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + expectResult: *documents + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collection + filter: {} + batchSize: 2 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$exists: true } + commandName: getMore + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$exists: true } + commandName: getMore + + - description: createCommandCursor creates a cursor and stores it as an entity that can be iterated one document at a time + operations: + - name: createCommandCursor + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + saveResultAsEntity: &myRunCommandCursor myRunCommandCursor + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 1, x: 11 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 2, x: 22 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 3, x: 33 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 4, x: 44 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 5, x: 55 } + + - description: createCommandCursor's cursor can be closed and will perform a killCursors operation + operations: + - name: createCommandCursor + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + saveResultAsEntity: myRunCommandCursor + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 1, x: 11 } + - name: close + object: *myRunCommandCursor + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collection + filter: {} + batchSize: 2 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + killCursors: *collection + cursors: { $$type: array } + commandName: killCursors diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.json b/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.json index 88fc28e34..b17ae78b9 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.json +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.json @@ -93,7 +93,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml b/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml index d84fd7689..508e594a5 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml @@ -54,6 +54,6 @@ tests: databaseName: *database0Name - commandStartedEvent: command: - getMore: { $$type: long } + getMore: { $$type: [ int, long ] } collection: *collection0Name commandName: getMore diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.json b/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.json index 85b8f69d7..6f955d81f 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.json +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.json @@ -109,7 +109,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "ns": { "$$type": "string" @@ -126,7 +129,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -138,7 +144,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "ns": { "$$type": "string" diff --git a/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.yml b/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.yml index 61c9f8835..3ecdf6da1 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.yml +++ b/src/test/spec/json/unified-test-format/valid-pass/entity-find-cursor.yml @@ -61,19 +61,19 @@ tests: - commandSucceededEvent: reply: cursor: - id: { $$type: long } + id: { $$type: [ int, long ] } ns: { $$type: string } firstBatch: { $$type: array } commandName: find - commandStartedEvent: command: - getMore: { $$type: long } + getMore: { $$type: [ int, long ] } collection: *collection0Name commandName: getMore - commandSucceededEvent: reply: cursor: - id: { $$type: long } + id: { $$type: [ int, long ] } ns: { $$type: string } nextBatch: { $$type: array } commandName: getMore diff --git a/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.json b/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.json new file mode 100644 index 000000000..9c6beda58 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.json @@ -0,0 +1,74 @@ +{ + "description": "expectedError-isClientError", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "single", + "replicaset" + ] + }, + { + "minServerVersion": "4.1.7", + "topologies": [ + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + } + ], + "tests": [ + { + "description": "isClientError considers network errors", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "ping" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isClientError": true + } + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.yml b/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.yml new file mode 100644 index 000000000..3bc12e73f --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/expectedError-isClientError.yml @@ -0,0 +1,39 @@ +description: "expectedError-isClientError" + +schemaVersion: "1.3" + +runOnRequirements: + - minServerVersion: "4.0" + topologies: [single, replicaset] + - minServerVersion: "4.1.7" + topologies: [sharded, load-balanced] + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name test + +tests: + - description: "isClientError considers network errors" + operations: + - name: failPoint + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ ping ] + closeConnection: true + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isClientError: true diff --git a/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json b/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json new file mode 100644 index 000000000..cf7bd6082 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json @@ -0,0 +1,68 @@ +{ + "description": "expectedEventsForClient-topologyDescriptionChangedEvent", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "can assert on values of newDescription and previousDescription fields", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "directConnection": true + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Single" + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.yml b/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.yml new file mode 100644 index 000000000..c8dacc391 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.yml @@ -0,0 +1,40 @@ +description: "expectedEventsForClient-topologyDescriptionChangedEvent" + +schemaVersion: "1.20" + +runOnRequirements: + - topologies: + - replicaset + minServerVersion: "4.4" # awaitable hello + +tests: + - description: "can assert on values of newDescription and previousDescription fields" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: &client client + uriOptions: + directConnection: true + observeEvents: + - topologyDescriptionChangedEvent + - name: waitForEvent + object: testRunner + arguments: + client: *client + event: + topologyDescriptionChangedEvent: {} + count: 1 + expectEvents: + - client: *client + eventType: sdam + ignoreExtraEvents: true + events: + - topologyDescriptionChangedEvent: + previousDescription: + type: "Unknown" + newDescription: + type: "Single" + diff --git a/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.json b/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.json new file mode 100644 index 000000000..93b25c983 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.json @@ -0,0 +1,10 @@ +{ + "description": "operation-empty_array", + "schemaVersion": "1.0", + "tests": [ + { + "description": "Empty operations array", + "operations": [] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.yml b/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.yml new file mode 100644 index 000000000..35d5ba371 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operation-empty_array.yml @@ -0,0 +1,7 @@ +description: "operation-empty_array" + +schemaVersion: "1.0" + +tests: + - description: "Empty operations array" + operations: [] diff --git a/src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.json b/src/test/spec/json/unified-test-format/valid-pass/operator-lte.json similarity index 79% rename from src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.json rename to src/test/spec/json/unified-test-format/valid-pass/operator-lte.json index 4de65c583..7a6a8057a 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.json +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-lte.json @@ -1,5 +1,5 @@ { - "description": "matches-lte-operator", + "description": "operator-lte", "schemaVersion": "1.9", "createEntities": [ { @@ -42,7 +42,9 @@ "arguments": { "document": { "_id": 1, - "y": 1 + "x": 2, + "y": 3, + "z": 4 } } } @@ -58,10 +60,18 @@ "documents": [ { "_id": { - "$$lte": 1 + "$$lte": 2 + }, + "x": { + "$$lte": 2.1 }, "y": { - "$$lte": 2 + "$$lte": { + "$numberLong": "3" + } + }, + "z": { + "$$lte": 4 } } ] diff --git a/src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.yml b/src/test/spec/json/unified-test-format/valid-pass/operator-lte.yml similarity index 79% rename from src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.yml rename to src/test/spec/json/unified-test-format/valid-pass/operator-lte.yml index 4bec571f0..87c4ece92 100644 --- a/src/test/spec/json/unified-test-format/valid-pass/matches-lte-operator.yml +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-lte.yml @@ -1,6 +1,6 @@ -description: matches-lte-operator +description: operator-lte -# Note: $$lte is not technically in the 1.8 schema but was introduced at the same time. +# Note: $$lte was introduced alongside schema changes for CSOT schemaVersion: "1.9" createEntities: @@ -27,7 +27,7 @@ tests: - name: insertOne object: *collection0 arguments: - document: { _id : 1, y: 1 } + document: { _id : 1, x: 2, y: 3, z: 4 } expectEvents: - client: *client0 events: @@ -36,6 +36,6 @@ tests: insert: *collection0Name documents: # We can make exact assertions here but we use the $$lte operator to ensure drivers support it. - - { _id: { $$lte: 1 }, y: { $$lte: 2 } } + - { _id: { $$lte: 2 }, x: { $$lte: 2.1 }, y: { $$lte: { $numberLong: "3"} }, z: { $$lte: 4 } } commandName: insert databaseName: *database0Name diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.json b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.json new file mode 100644 index 000000000..fd8b514d4 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.json @@ -0,0 +1,124 @@ +{ + "description": "operator-matchAsDocument", + "schemaVersion": "1.13", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "test", + "documents": [ + { + "_id": 1, + "json": "{ \"x\": 1, \"y\": 2.0 }" + }, + { + "_id": 2, + "json": "{ \"x\": { \"$oid\": \"57e193d7a9cc81b4027498b5\" } }" + } + ] + } + ], + "tests": [ + { + "description": "matchAsDocument performs flexible numeric comparisons", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "json": { + "$$matchAsDocument": { + "x": 1, + "y": 2 + } + } + } + ] + } + ] + }, + { + "description": "matchAsDocument evaluates special operators", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "json": { + "$$matchAsDocument": { + "x": 1, + "y": { + "$$exists": true + } + } + } + } + ] + } + ] + }, + { + "description": "matchAsDocument decodes Extended JSON", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 2, + "json": { + "$$matchAsDocument": { + "x": { + "$$type": "objectId" + } + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.yml b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.yml new file mode 100644 index 000000000..9a811faaa --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsDocument.yml @@ -0,0 +1,54 @@ +description: operator-matchAsDocument + +schemaVersion: "1.13" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name test + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, json: '{ "x": 1, "y": 2.0 }' } + - { _id: 2, json: '{ "x": { "$oid": "57e193d7a9cc81b4027498b5" } }' } + +tests: + - + description: matchAsDocument performs flexible numeric comparisons + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 1 } + limit: 1 + expectResult: + - { _id: 1, json: { $$matchAsDocument: { x: 1.0, y: 2 } } } + - + description: matchAsDocument evaluates special operators + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 1 } + limit: 1 + expectResult: + - { _id: 1, json: { $$matchAsDocument: { x: 1, y: { $$exists: true } } } } + - + description: matchAsDocument decodes Extended JSON + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 2 } + limit: 1 + expectResult: + - { _id: 2, json: { $$matchAsDocument: { x: { $$type: "objectId" } } } } diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.json b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.json new file mode 100644 index 000000000..1966e3b37 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.json @@ -0,0 +1,151 @@ +{ + "description": "operator-matchAsRoot", + "schemaVersion": "1.13", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "test", + "documents": [ + { + "_id": 1, + "x": { + "y": 2, + "z": 3 + } + }, + { + "_id": 2, + "json": "{ \"x\": 1, \"y\": 2 }" + } + ] + } + ], + "tests": [ + { + "description": "matchAsRoot with nested document", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$matchAsRoot": { + "y": 2 + } + } + } + ] + } + ] + }, + { + "description": "matchAsRoot performs flexible numeric comparisons", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$matchAsRoot": { + "y": 2 + } + } + } + ] + } + ] + }, + { + "description": "matchAsRoot evaluates special operators", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$matchAsRoot": { + "y": 2, + "z": { + "$$exists": true + } + } + } + } + ] + } + ] + }, + { + "description": "matchAsRoot with matchAsDocument", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 2, + "json": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "x": 1 + } + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.yml b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.yml new file mode 100644 index 000000000..bbf738f04 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-matchAsRoot.yml @@ -0,0 +1,64 @@ +description: operator-matchAsRoot + +schemaVersion: "1.13" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name test + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: { y: 2, z: 3 } } + - { _id: 2, json: '{ "x": 1, "y": 2 }' } + +tests: + - + description: matchAsRoot with nested document + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 1 } + limit: 1 + expectResult: + - { _id: 1, x: { $$matchAsRoot: { y: 2 } } } + - + description: matchAsRoot performs flexible numeric comparisons + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 1 } + limit: 1 + expectResult: + - { _id: 1, x: { $$matchAsRoot: { y: 2.0 } } } + - + description: matchAsRoot evaluates special operators + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 1 } + limit: 1 + expectResult: + - { _id: 1, x: { $$matchAsRoot: { y: 2, z: { $$exists: true } } } } + - + description: matchAsRoot with matchAsDocument + operations: + - name: find + object: *collection0 + arguments: + filter: { _id : 2 } + limit: 1 + expectResult: + - { _id: 2, json: { $$matchAsDocument: { $$matchAsRoot: { x: 1 } } } } diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.json b/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.json new file mode 100644 index 000000000..e628d0d77 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.json @@ -0,0 +1,174 @@ +{ + "description": "operator-type-number_alias", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "test", + "documents": [] + } + ], + "tests": [ + { + "description": "type number alias matches int32", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": { + "$numberInt": "2147483647" + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$type": "number" + } + } + ] + } + ] + }, + { + "description": "type number alias matches int64", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": { + "$numberLong": "9223372036854775807" + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$type": "number" + } + } + ] + } + ] + }, + { + "description": "type number alias matches double", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": { + "$numberDouble": "2.71828" + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$type": "number" + } + } + ] + } + ] + }, + { + "description": "type number alias matches decimal128", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": { + "$numberDecimal": "3.14159" + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "limit": 1 + }, + "expectResult": [ + { + "_id": 1, + "x": { + "$$type": "number" + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.yml b/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.yml new file mode 100644 index 000000000..04357a024 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/operator-type-number_alias.yml @@ -0,0 +1,61 @@ +description: operator-type-number_alias + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name test + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: [] + +tests: + - + description: type number alias matches int32 + operations: + - name: insertOne + object: *collection0 + arguments: + document: { _id: 1, x: { $numberInt: "2147483647" } } + - &find + name: find + object: *collection0 + arguments: + filter: { _id: 1 } + limit: 1 + expectResult: + - { _id: 1, x: { $$type: "number" } } + - + description: type number alias matches int64 + operations: + - name: insertOne + object: *collection0 + arguments: + document: { _id: 1, x: { $numberLong: "9223372036854775807" } } + - *find + - + description: type number alias matches double + operations: + - name: insertOne + object: *collection0 + arguments: + document: { _id: 1, x: { $numberDouble: "2.71828" } } + - *find + - + description: type number alias matches decimal128 + operations: + - name: insertOne + object: *collection0 + arguments: + document: { _id: 1, x: { $numberDecimal: "3.14159" } } + - *find diff --git a/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.json b/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.json new file mode 100644 index 000000000..b85bfffb9 --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.json @@ -0,0 +1,163 @@ +{ + "description": "poc-queryable-encryption", + "schemaVersion": "1.23", + "runOnRequirements": [ + { + "minServerVersion": "7.0", + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + } + } + }, + { + "database": { + "id": "encryptedDB", + "client": "client0", + "databaseName": "poc-queryable-encryption" + } + }, + { + "collection": { + "id": "encryptedColl", + "database": "encryptedDB", + "collectionName": "encrypted" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1641024000000" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1641024000000" + } + }, + "status": 1, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "poc-queryable-encryption", + "collectionName": "encrypted", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedInt", + "bsonType": "int", + "queries": { + "queryType": "equality", + "contention": { + "$numberLong": "0" + } + } + } + ] + } + } + } + ], + "tests": [ + { + "description": "insert, replace, and find with queryable encryption", + "operations": [ + { + "object": "encryptedColl", + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedInt": 11 + } + } + }, + { + "object": "encryptedColl", + "name": "replaceOne", + "arguments": { + "filter": { + "encryptedInt": 11 + }, + "replacement": { + "encryptedInt": 22 + } + } + }, + { + "object": "encryptedColl", + "name": "find", + "arguments": { + "filter": { + "encryptedInt": 22 + } + }, + "expectResult": [ + { + "_id": 1, + "encryptedInt": 22 + } + ] + } + ], + "outcome": [ + { + "collectionName": "encrypted", + "databaseName": "poc-queryable-encryption", + "documents": [ + { + "_id": 1, + "encryptedInt": { + "$$type": "binData" + }, + "__safeContent__": { + "$$type": "array" + } + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.yml b/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.yml new file mode 100644 index 000000000..8b5f6c46b --- /dev/null +++ b/src/test/spec/json/unified-test-format/valid-pass/poc-queryable-encryption.yml @@ -0,0 +1,73 @@ +description: poc-queryable-encryption + +schemaVersion: "1.23" + +runOnRequirements: + - minServerVersion: "7.0" + csfle: true + +createEntities: + - client: + id: &client0 client0 + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + - database: + id: &encryptedDB encryptedDB + client: *client0 + databaseName: &encryptedDBName poc-queryable-encryption + - collection: + id: &encryptedColl encryptedColl + database: *encryptedDB + collectionName: &encryptedCollName encrypted + +initialData: + - databaseName: keyvault + collectionName: datakeys + documents: + - _id: &keyid { $binary: { base64: EjRWeBI0mHYSNBI0VniQEg==, subType: "04" } } + keyMaterial: { $binary: { base64: sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==, subType: "00" } } + creationDate: { $date: { $numberLong: "1641024000000" } } + updateDate: { $date: { $numberLong: "1641024000000" } } + status: 1 + masterKey: + provider: local + - databaseName: *encryptedDBName + collectionName: *encryptedCollName + documents: [] + createOptions: + encryptedFields: + fields: + - keyId: *keyid + path: 'encryptedInt' + bsonType: 'int' + queries: {'queryType': 'equality', 'contention': {'$numberLong': '0'}} + +tests: + - description: insert, replace, and find with queryable encryption + operations: + - object: *encryptedColl + name: insertOne + arguments: + document: + _id: 1 + encryptedInt: 11 + - object: *encryptedColl + name: replaceOne + arguments: + filter: { encryptedInt: 11 } + replacement: { encryptedInt: 22 } + - object: *encryptedColl + name: find + arguments: + filter: { encryptedInt: 22 } + expectResult: + - _id: 1 + encryptedInt: 22 + outcome: + - collectionName: *encryptedCollName + databaseName: *encryptedDBName + documents: + - { _id: 1, encryptedInt: { $$type: binData }, __safeContent__: { $$type: array} } \ No newline at end of file diff --git a/src/test/spec/unified_runner.rs b/src/test/spec/unified_runner.rs index 3a1439f08..577cee274 100644 --- a/src/test/spec/unified_runner.rs +++ b/src/test/spec/unified_runner.rs @@ -134,6 +134,8 @@ async fn valid_pass() { // TODO: unskip this file when the convenient transactions API tests are converted to the // unified format "poc-transactions-convenient-api.json", + // TODO RUST-2077: unskip this file + "poc-queryable-encryption.json", ]; // These tests need the in-use-encryption feature flag to be deserialized and run. if cfg!(not(feature = "in-use-encryption")) { diff --git a/src/test/spec/unified_runner/matcher.rs b/src/test/spec/unified_runner/matcher.rs index 167a76618..6d75ba376 100644 --- a/src/test/spec/unified_runner/matcher.rs +++ b/src/test/spec/unified_runner/matcher.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use crate::{ bson::{doc, spec::ElementType, Bson, Document}, - bson_util::get_int, + bson_util::{get_double, get_int}, event::{ cmap::CmapEvent, command::CommandEvent, @@ -574,6 +574,21 @@ fn special_operator_matches( results_match_inner(Some(&doc), value, false, false, entities) } "$$matchAsRoot" => results_match_inner(actual, value, false, true, entities), + "$$lte" => { + let Some(expected) = get_double(value) else { + return Err(format!("expected number for comparison, got {}", value)); + }; + let Some(actual) = actual.and_then(get_double) else { + return Err(format!("expected actual to be a number, got {:?}", actual)); + }; + if actual > expected { + return Err(format!( + "expected actual to be <= {}, got {}", + expected, actual + )); + } + Ok(()) + } other => panic!("unknown special operator: {}", other), } } @@ -593,6 +608,10 @@ fn type_matches(types: &Bson, actual: &Bson) -> Result<(), String> { } } Bson::String(str) => { + if str == "number" { + let number_types: Bson = vec!["int", "long", "double", "decimal"].into(); + return type_matches(&number_types, actual); + } let expected = match str.as_ref() { "double" => ElementType::Double, "string" => ElementType::String, diff --git a/src/test/spec/unified_runner/test_file.rs b/src/test/spec/unified_runner/test_file.rs index 60e240533..c66032bfd 100644 --- a/src/test/spec/unified_runner/test_file.rs +++ b/src/test/spec/unified_runner/test_file.rs @@ -1,6 +1,5 @@ -use std::{borrow::Cow, collections::HashMap, fmt::Write, sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; -use percent_encoding::NON_ALPHANUMERIC; use pretty_assertions::assert_eq; use regex::Regex; use semver::Version; @@ -240,7 +239,13 @@ pub(crate) fn merge_uri_options( uri_options: Option<&Document>, use_multiple_hosts: bool, ) -> String { - let given_uri = if !use_multiple_hosts && !given_uri.starts_with("mongodb+srv") { + let direct_connection = uri_options + .and_then(|options| options.get_bool("directConnection").ok()) + .unwrap_or(false); + + // TODO RUST-1308: use the ConnectionString type to remove hosts + let uri = if (!use_multiple_hosts || direct_connection) && !given_uri.starts_with("mongodb+srv") + { let hosts_regex = Regex::new(r"mongodb://([^/]*)").unwrap(); let single_host = hosts_regex .captures(given_uri) @@ -251,55 +256,50 @@ pub(crate) fn merge_uri_options( .split(',') .next() .expect("expected URI to contain at least one host, but it had none"); - hosts_regex.replace(given_uri, format!("mongodb://{}", single_host)) + hosts_regex + .replace(given_uri, format!("mongodb://{}", single_host)) + .to_string() } else { - Cow::Borrowed(given_uri) + given_uri.to_string() }; - let uri_options = match uri_options { - Some(opts) => opts, - None => return given_uri.to_string(), + let Some(mut uri_options) = uri_options.cloned() else { + return uri; }; - let mut given_uri_parts = given_uri.split('?'); + let (mut uri, existing_options) = match uri.split_once("?") { + Some((pre_options, options)) => (pre_options.to_string(), Some(options)), + None => (uri, None), + }; - let mut uri = String::from(given_uri_parts.next().unwrap()); - // A connection string has two slashes before the host list and one slash before the auth db - // name. If an auth db name is not provided the latter slash might not be present, so it needs - // to be added manually. - if uri.chars().filter(|c| *c == '/').count() < 3 { - uri.push('/'); - } - uri.push('?'); - - if let Some(options) = given_uri_parts.next() { - let options = options.split('&'); - for option in options { - let key = option.split('=').next().unwrap(); - // The provided URI options should override any existing options in the connection - // string. + if let Some(existing_options) = existing_options { + for option in existing_options.split("&") { + let (key, value) = option.split_once("=").unwrap(); + // prefer the option specified by the test if !uri_options.contains_key(key) { - uri.push_str(option); - uri.push('&'); + uri_options.insert(key, value); } } } + if direct_connection { + uri_options.remove("replicaSet"); + } + + let mut join = '?'; for (key, value) in uri_options { + uri.push(join); + if join == '?' { + join = '&'; + } + uri.push_str(&key); + uri.push('='); + let value = value.to_string(); - // to_string() wraps quotations around Bson strings let value = value.trim_start_matches('\"').trim_end_matches('\"'); - let _ = write!( - &mut uri, - "{}={}&", - &key, - percent_encoding::percent_encode(value.as_bytes(), NON_ALPHANUMERIC) - ); + uri.push_str(value); } - // remove the trailing '&' from the URI (or '?' if no options are present) - uri.pop(); - uri }