Skip to content

Commit 2daaa99

Browse files
author
Divjot Arora
committed
DRIVERS-555 Add spec and prose tests for CSOT
1 parent ed4e62b commit 2daaa99

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+23561
-282
lines changed

.github/workflows/json-regenerate-check.yml

+1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ jobs:
2929
python3 ./source/server-discovery-and-monitoring/tests/errors/generate-error-tests.py
3030
python3 ./source/client-side-encryption/etc/generate-corpus.py ./source/client-side-encryption/corpus
3131
python3 ./source/client-side-encryption/etc/generate-test.py ./source/client-side-encryption/etc/test-templates/*.template ./source/client-side-encryption/tests
32+
python3 ./source/client-side-operations-timeout/etc/generate-basic-tests.py ./source/client-side-operations-timeout/etc/templates ./source/client-side-operations-timeout/tests
3233
cd source && make && git diff --exit-code
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
runOn:
2+
- minServerVersion: "4.4"
3+
database_name: &database_name "cse-timeouts-db"
4+
collection_name: &collection_name "cse-timeouts-coll"
5+
6+
data: []
7+
json_schema: {{schema()}}
8+
key_vault_data: [{{key()}}]
9+
10+
tests:
11+
- description: "timeoutMS applied to listCollections to get collection schema"
12+
failPoint:
13+
configureFailPoint: failCommand
14+
mode: { times: 1 }
15+
data:
16+
failCommands: ["listCollections"]
17+
blockConnection: true
18+
blockTimeMS: 60
19+
clientOptions:
20+
autoEncryptOpts:
21+
kmsProviders:
22+
aws: {} # Credentials filled in from environment.
23+
timeoutMS: 50
24+
operations:
25+
- name: insertOne
26+
arguments:
27+
document: &doc0 { _id: 1, encrypted_string: "string0", random: "abc" }
28+
result:
29+
isTimeoutError: true
30+
expectations:
31+
# Auto encryption will request the collection info.
32+
- command_started_event:
33+
command:
34+
listCollections: 1
35+
filter:
36+
name: *collection_name
37+
maxTimeMS: { $$type: ["int", "long"] }
38+
command_name: listCollections
39+
40+
# Test that timeoutMS applies to the sum of all operations done for client-side encryption. This is done by blocking
41+
# listCollections and find for 20ms each and running an insertOne with timeoutMS=50. There should be two
42+
# listCollections commands and one "find" command, so the sum should take more than timeoutMS.
43+
#
44+
# This test does not include command monitoring expectations because the exact command sequence is dependent on the
45+
# amount of time taken by mongocryptd communication. In slow runs, mongocryptd communication can breach the timeout
46+
# and result in the final "find" not being sent.
47+
- description: "remaining timeoutMS applied to find to get keyvault data"
48+
failPoint:
49+
configureFailPoint: failCommand
50+
mode: { times: 3 }
51+
data:
52+
failCommands: ["listCollections", "find"]
53+
blockConnection: true
54+
blockTimeMS: 20
55+
clientOptions:
56+
autoEncryptOpts:
57+
kmsProviders:
58+
aws: {} # Credentials filled in from environment.
59+
timeoutMS: 50
60+
operations:
61+
- name: insertOne
62+
arguments:
63+
document: *doc0
64+
result:
65+
isTimeoutError: true

source/client-side-encryption/tests/README.rst

+28-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ Client Side Encryption Tests
99
Introduction
1010
============
1111

12-
This document describes the format of the driver spec tests included in the JSON
13-
and YAML files included in this directory.
12+
This document describes the format of the driver spec tests included in the
13+
JSON and YAML files included in this directory. The
14+
``timeoutMS.yml``/``timeoutMS.json`` files in this directory contain tests
15+
for the ``timeoutMS`` option and its application to the client-side
16+
encryption feature. Drivers MUST only run these tests after implementing the
17+
`Client Side Operations Timeout
18+
<../client-side-operations-timeout/client-side-operations-timeout.rst>`__
19+
specification.
1420

1521
Additional prose tests, that are not represented in the spec tests, are described
1622
and MUST be implemented by all drivers.
@@ -30,7 +36,8 @@ The spec tests format is an extension of `transactions spec tests <https://githu
3036

3137
- Addition of `$$type` to command_started_event and outcome.
3238

33-
The semantics of `$$type` is that any actual value matching the BSON type indicated by the BSON type string is considered a match.
39+
The semantics of `$$type` is that any actual value matching one of the types indicated by either a BSON type string
40+
or an array of BSON type strings is considered a match.
3441

3542
For example, the following matches a command_started_event for an insert of a document where `random` must be of type ``binData``::
3643

@@ -42,6 +49,16 @@ For example, the following matches a command_started_event for an insert of a do
4249
ordered: true
4350
command_name: insert
4451

52+
The following matches a command_started_event for an insert of a document where ``random`` must be of type
53+
``binData`` or ``string``::
54+
55+
- command_started_event:
56+
command:
57+
insert: *collection_name
58+
documents:
59+
- { random: { $$type: ["binData", "string"] } }
60+
ordered: true
61+
command_name: insert
4562

4663
The values of `$$type` correspond to `these documented string representations of BSON types <https://docs.mongodb.com/manual/reference/bson-types/>`_.
4764

@@ -69,6 +86,10 @@ Each YAML file has the following keys:
6986

7087
- ``skipReason``: |txn|
7188

89+
- ``useMultipleMongoses``: |txn|
90+
91+
- ``failPoint``: |txn|
92+
7293
- ``clientOptions``: Optional, parameters to pass to MongoClient().
7394

7495
- ``autoEncryptOpts``: Optional
@@ -104,7 +125,10 @@ Each YAML file has the following keys:
104125

105126
- ``arguments``: |txn|
106127

107-
- ``result``: |txn|
128+
- ``result``: Same as the Transactions spec test format with one addition: if the operation is expected to return
129+
an error, the ``result`` document may contain an ``isTimeoutError`` boolean field. If ``true``, the test runner
130+
MUST assert that the error represents a timeout due to the use of the ``timeoutMS`` operation. If ``false``, the
131+
test runner MUST assert that the error does not represent a timeout.
108132

109133
- ``expectations``: |txn|
110134

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
runOn:
2+
- minServerVersion: "4.4"
3+
database_name: &database_name "cse-timeouts-db"
4+
collection_name: &collection_name "cse-timeouts-coll"
5+
6+
data: []
7+
json_schema: {'properties': {'encrypted_w_altname': {'encrypt': {'keyId': '/altname', 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'}}, 'encrypted_string': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'}}, 'random': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'}}, 'encrypted_string_equivalent': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'}}}, 'bsonType': 'object'}
8+
key_vault_data: [{'status': 1, '_id': {'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}, 'masterKey': {'provider': 'aws', 'key': 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'region': 'us-east-1'}, 'updateDate': {'$date': {'$numberLong': '1552949630483'}}, 'keyMaterial': {'$binary': {'base64': 'AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO', 'subType': '00'}}, 'creationDate': {'$date': {'$numberLong': '1552949630483'}}, 'keyAltNames': ['altname', 'another_altname']}]
9+
10+
tests:
11+
- description: "timeoutMS applied to listCollections to get collection schema"
12+
failPoint:
13+
configureFailPoint: failCommand
14+
mode: { times: 1 }
15+
data:
16+
failCommands: ["listCollections"]
17+
blockConnection: true
18+
blockTimeMS: 60
19+
clientOptions:
20+
autoEncryptOpts:
21+
kmsProviders:
22+
aws: {} # Credentials filled in from environment.
23+
timeoutMS: 50
24+
operations:
25+
- name: insertOne
26+
arguments:
27+
document: &doc0 { _id: 1, encrypted_string: "string0", random: "abc" }
28+
result:
29+
isTimeoutError: true
30+
expectations:
31+
# Auto encryption will request the collection info.
32+
- command_started_event:
33+
command:
34+
listCollections: 1
35+
filter:
36+
name: *collection_name
37+
maxTimeMS: { $$type: ["int", "long"] }
38+
command_name: listCollections
39+
40+
# Test that timeoutMS applies to the sum of all operations done for client-side encryption. This is done by blocking
41+
# listCollections and find for 20ms each and running an insertOne with timeoutMS=50. There should be two
42+
# listCollections commands and one "find" command, so the sum should take more than timeoutMS.
43+
#
44+
# This test does not include command monitoring expectations because the exact command sequence is dependent on the
45+
# amount of time taken by mongocryptd communication. In slow runs, mongocryptd communication can breach the timeout
46+
# and result in the final "find" not being sent.
47+
- description: "remaining timeoutMS applied to find to get keyvault data"
48+
failPoint:
49+
configureFailPoint: failCommand
50+
mode: { times: 3 }
51+
data:
52+
failCommands: ["listCollections", "find"]
53+
blockConnection: true
54+
blockTimeMS: 20
55+
clientOptions:
56+
autoEncryptOpts:
57+
kmsProviders:
58+
aws: {} # Credentials filled in from environment.
59+
timeoutMS: 50
60+
operations:
61+
- name: insertOne
62+
arguments:
63+
document: *doc0
64+
result:
65+
isTimeoutError: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from collections import namedtuple
2+
from jinja2 import Template
3+
import os
4+
import sys
5+
6+
Operation = namedtuple('Operation', ['operation_name', 'command_name', 'object', 'arguments'])
7+
8+
CLIENT_OPERATIONS = [
9+
Operation('listDatabases', 'listDatabases', 'client', ['filter: {}']),
10+
Operation('listDatabaseNames', 'listDatabases', 'client', ['filter: {}']),
11+
Operation('createChangeStream', 'aggregate', 'client', ['pipeline: []'])
12+
]
13+
14+
DB_OPERATIONS = [
15+
Operation('aggregate', 'aggregate', 'database', ['pipeline: [ { $listLocalSessions: {} }, { $limit: 1 } ]']),
16+
Operation('listCollections', 'listCollections', 'database', ['filter: {}']),
17+
Operation('listCollectionNames', 'listCollections', 'database', ['filter: {}']),
18+
Operation('runCommand', 'ping', 'database', ['command: { ping: 1 }']),
19+
Operation('createChangeStream', 'aggregate', 'database', ['pipeline: []'])
20+
]
21+
22+
INSERT_MANY_ARGUMENTS = '''documents:
23+
- { x: 1 }'''
24+
25+
BULK_WRITE_ARGUMENTS = '''requests:
26+
- insertOne:
27+
document: { _id: 1 }'''
28+
29+
COLLECTION_OPERATIONS = [
30+
Operation('aggregate', 'aggregate', 'collection', ['pipeline: []']),
31+
Operation('count', 'count', 'collection', ['filter: {}']),
32+
Operation('countDocuments', 'aggregate', 'collection', ['filter: {}']),
33+
Operation('estimatedDocumentCount', 'count', 'collection', []),
34+
Operation('distinct', 'distinct', 'collection', ['fieldName: x', 'filter: {}']),
35+
Operation('find', 'find', 'collection', ['filter: {}']),
36+
Operation('findOne', 'find', 'collection', ['filter: {}']),
37+
Operation('listIndexes', 'listIndexes', 'collection', []),
38+
Operation('listIndexNames', 'listIndexes', 'collection', []),
39+
Operation('createChangeStream', 'aggregate', 'collection', ['pipeline: []']),
40+
Operation('insertOne', 'insert', 'collection', ['document: { x: 1 }']),
41+
Operation('insertMany', 'insert', 'collection', [INSERT_MANY_ARGUMENTS]),
42+
Operation('deleteOne', 'delete', 'collection', ['filter: {}']),
43+
Operation('deleteMany', 'delete', 'collection', ['filter: {}']),
44+
Operation('replaceOne', 'update', 'collection', ['filter: {}', 'replacement: { x: 1 }']),
45+
Operation('updateOne', 'update', 'collection', ['filter: {}', 'update: { $set: { x: 1 } }']),
46+
Operation('updateMany', 'update', 'collection', ['filter: {}', 'update: { $set: { x: 1 } }']),
47+
Operation('findOneAndDelete', 'findAndModify', 'collection', ['filter: {}']),
48+
Operation('findOneAndReplace', 'findAndModify', 'collection', ['filter: {}', 'replacement: { x: 1 }']),
49+
Operation('findOneAndUpdate', 'findAndModify', 'collection', ['filter: {}', 'update: { $set: { x: 1 } }']),
50+
Operation('bulkWrite', 'insert', 'collection', [BULK_WRITE_ARGUMENTS]),
51+
Operation('createIndex', 'createIndexes', 'collection', ['keys: { x: 1 }', 'name: "x_1"']),
52+
Operation('dropIndex', 'dropIndexes', 'collection', ['name: "x_1"']),
53+
Operation('dropIndexes', 'dropIndexes', 'collection', []),
54+
]
55+
56+
# Session and GridFS operations are generally tested in other files, so they're not included in the list of all
57+
# operations. Individual generation functions can choose to include them if needed.
58+
OPERATIONS = CLIENT_OPERATIONS + DB_OPERATIONS + COLLECTION_OPERATIONS
59+
60+
RETRYABLE_WRITE_OPERATIONS = [op for op in OPERATIONS if op.operation_name in
61+
['insertOne', 'updateOne', 'deleteOne', 'replaceOne', 'findOneAndDelete', 'findOneAndUpdate', 'findOneAndReplace', 'insertMany', 'bulkWrite']
62+
]
63+
64+
RETRYABLE_READ_OPERATIONS = [op for op in OPERATIONS if op.operation_name in
65+
['find', 'findOne', 'aggregate', 'distinct', 'count', 'estimatedDocumentCount', 'countDocuments', 'createChangeStream', 'listDatabases',
66+
'listDatabaseNames', 'listCollections', 'listCollectionNames', 'listIndexes']
67+
]
68+
69+
templates_dir = sys.argv[1]
70+
tests_dir = sys.argv[2]
71+
72+
def get_template(file):
73+
path = f'{templates_dir}/{file}.yml.template'
74+
return Template(open(path, 'r').read())
75+
76+
def write_yaml(file, template, injections):
77+
rendered = template.render(**injections)
78+
path = f'{tests_dir}/{file}.yml'
79+
open(path, 'w').write(rendered)
80+
81+
def get_command_object(object):
82+
if object == 'client' or object == 'database':
83+
return 1
84+
return '*collectionName'
85+
86+
def max_time_supported(operation_name):
87+
return operation_name in ['aggregate', 'count', 'estimatedDocumentCount', 'distinct', 'find', 'findOne',
88+
'findOneAndDelete', 'findOneAndReplace', 'findOneAndUpdate', 'createIndex', 'dropIndex', 'dropIndexes']
89+
90+
def generate(name, operations):
91+
template = get_template(name)
92+
injections = {
93+
'operations': operations,
94+
'get_command_object': get_command_object,
95+
'max_time_supported': max_time_supported,
96+
}
97+
write_yaml(name, template, injections)
98+
99+
def generate_global_timeout_tests():
100+
generate('global-timeoutMS', OPERATIONS)
101+
102+
def generate_override_db():
103+
generate('override-database-timeoutMS', DB_OPERATIONS + COLLECTION_OPERATIONS)
104+
105+
def generate_override_coll():
106+
generate('override-collection-timeoutMS', COLLECTION_OPERATIONS)
107+
108+
def generate_override_operation():
109+
generate('override-operation-timeoutMS', OPERATIONS)
110+
111+
def generate_retryable():
112+
generate('retryability-timeoutMS', RETRYABLE_WRITE_OPERATIONS + RETRYABLE_READ_OPERATIONS)
113+
generate('retryability-legacy-timeouts', RETRYABLE_WRITE_OPERATIONS + RETRYABLE_READ_OPERATIONS)
114+
115+
def generate_deprecated():
116+
generate('deprecated-options', OPERATIONS)
117+
118+
generate_global_timeout_tests()
119+
generate_override_db()
120+
generate_override_coll()
121+
generate_override_operation()
122+
generate_retryable()
123+
generate_deprecated()

0 commit comments

Comments
 (0)