diff --git a/.evergreen/config.yml b/.evergreen/config.yml index dcb2f4688..e4c99ca80 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -290,6 +290,7 @@ functions: STORAGE_ENGINE=${STORAGE_ENGINE} \ DISABLE_TEST_COMMANDS=${DISABLE_TEST_COMMANDS} \ ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ sh ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh # run-orchestration generates expansion file with the MONGODB_URI for the cluster - command: expansions.update @@ -425,6 +426,7 @@ functions: AUTH=${AUTH} \ SSL=${SSL} \ DATA_LAKE=${DATA_LAKE} \ + MONGODB_API_VERSION=${MONGODB_API_VERSION} \ sh ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh "run enterprise auth tests": @@ -2023,6 +2025,19 @@ axes: variables: SETDEFAULTENCODING: "cp1251" + - id: requireApiVersion + display_name: "requireApiVersion" + values: + - id: "requireApiVersion1" + display_name: "requireApiVersion1" + tags: [ "requireApiVersion_tag" ] + variables: + # REQUIRE_API_VERSION is set to make drivers-evergreen-tools + # start a cluster with the requireApiVersion parameter. + REQUIRE_API_VERSION: "1" + # MONGODB_API_VERSION is the apiVersion to use in the test suite. + MONGODB_API_VERSION: "1" + buildvariants: - matrix_name: "tests-all" matrix_spec: @@ -2605,6 +2620,17 @@ buildvariants: tasks: - name: atlas-data-lake-tests +- matrix_name: "versioned-api-tests" + matrix_spec: + platform: ubuntu-16.04 + python-version: ["2.7", "3.9"] + auth: "auth" + requireApiVersion: "*" + display_name: "requireApiVersion ${python-version}" + tasks: + # Versioned API was introduced in MongoDB 4.7 + - "test-latest-standalone" + - matrix_name: "ocsp-test" matrix_spec: platform: ubuntu-16.04 diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 50357d49d..d1ecfdcda 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -26,6 +26,7 @@ GREEN_FRAMEWORK=${GREEN_FRAMEWORK:-} C_EXTENSIONS=${C_EXTENSIONS:-} COVERAGE=${COVERAGE:-} COMPRESSORS=${COMPRESSORS:-} +MONGODB_API_VERSION=${MONGODB_API_VERSION:-} TEST_ENCRYPTION=${TEST_ENCRYPTION:-} LIBMONGOCRYPT_URL=${LIBMONGOCRYPT_URL:-} SETDEFAULTENCODING=${SETDEFAULTENCODING:-} @@ -35,6 +36,11 @@ if [ -n "$COMPRESSORS" ]; then export COMPRESSORS=$COMPRESSORS fi +if [ -n "$MONGODB_API_VERSION" ]; then + export MONGODB_API_VERSION=$MONGODB_API_VERSION +fi + + export JAVA_HOME=/opt/java/jdk8 if [ "$AUTH" != "noauth" ]; then diff --git a/doc/api/pymongo/index.rst b/doc/api/pymongo/index.rst index 9fc90a1e1..fb48c94ea 100644 --- a/doc/api/pymongo/index.rst +++ b/doc/api/pymongo/index.rst @@ -54,6 +54,7 @@ Sub-modules: read_preferences results son_manipulator + server_api uri_parser write_concern event_loggers diff --git a/doc/api/pymongo/server_api.rst b/doc/api/pymongo/server_api.rst new file mode 100644 index 000000000..d961d07f1 --- /dev/null +++ b/doc/api/pymongo/server_api.rst @@ -0,0 +1,11 @@ +:mod:`server_api` -- Support for MongoDB Versioned API +====================================================== + +.. automodule:: pymongo.server_api + :synopsis: Support for MongoDB Versioned API + + .. autoclass:: pymongo.server_api.ServerApi + :members: + + .. autoclass:: pymongo.server_api.ServerApiVersion + :members: diff --git a/doc/changelog.rst b/doc/changelog.rst index c9f4ef4dc..91d78e590 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -17,6 +17,11 @@ Breaking Changes in 4.0 - Removed support for Python 2.7, 3.4, and 3.5. Python 3.6+ is now required. - Removed :mod:`~pymongo.thread_util`. +Notable improvements +.................... + +- Support for MongoDB Versioned API, see :class:`~pymongo.server_api.ServerApi`. + Issues Resolved ............... diff --git a/pymongo/client_options.py b/pymongo/client_options.py index 9611bb0bd..8892526c7 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -125,6 +125,7 @@ def _parse_pool_options(options): event_listeners = options.get('event_listeners') appname = options.get('appname') driver = options.get('driver') + server_api = options.get('server_api') compression_settings = CompressionSettings( options.get('compressors', []), options.get('zlibcompressionlevel', -1)) @@ -138,7 +139,8 @@ def _parse_pool_options(options): _EventListeners(event_listeners), appname, driver, - compression_settings) + compression_settings, + server_api=server_api) class ClientOptions(object): diff --git a/pymongo/client_session.py b/pymongo/client_session.py index dec2f4f91..c88f3f277 100644 --- a/pymongo/client_session.py +++ b/pymongo/client_session.py @@ -297,6 +297,9 @@ class _Transaction(object): def active(self): return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS) + def starting(self): + return self.state == _TxnState.STARTING + def reset(self): self.state = _TxnState.NONE self.sharded = False @@ -762,6 +765,12 @@ class ClientSession(object): """ return self._transaction.active() + @property + def _starting_transaction(self): + """True if this session is starting a multi-statement transaction. + """ + return self._transaction.starting() + @property def _pinned_address(self): """The mongos address this transaction was created on.""" diff --git a/pymongo/common.py b/pymongo/common.py index 81555ef39..a4d91b7b1 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -27,6 +27,7 @@ from pymongo.auth import MECHANISMS from pymongo.compression_support import (validate_compressors, validate_zlib_compression_level) from pymongo.driver_info import DriverInfo +from pymongo.server_api import ServerApi from pymongo.encryption_options import validate_auto_encryption_opts_or_none from pymongo.errors import ConfigurationError from pymongo.monitoring import _validate_event_listeners @@ -528,6 +529,15 @@ def validate_driver_or_none(option, value): return value +def validate_server_api_or_none(option, value): + """Validate the server_api keyword arg.""" + if value is None: + return value + if not isinstance(value, ServerApi): + raise TypeError("%s must be an instance of ServerApi" % (option,)) + return value + + def validate_is_callable_or_none(option, value): """Validates that 'value' is a callable.""" if value is None: @@ -643,6 +653,7 @@ URI_OPTIONS_VALIDATOR_MAP = { NONSPEC_OPTIONS_VALIDATOR_MAP = { 'connect': validate_boolean_or_string, 'driver': validate_driver_or_none, + 'server_api': validate_server_api_or_none, 'fsync': validate_boolean_or_string, 'minpoolsize': validate_non_negative_integer, 'socketkeepalive': validate_boolean_or_string, diff --git a/pymongo/database.py b/pymongo/database.py index a31e918ac..42055ec81 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -704,6 +704,12 @@ class Database(common.BaseObject): .. note:: :meth:`command` does **not** apply any custom TypeDecoders when decoding the command response. + .. note:: If this client has been configured to use MongoDB Versioned + API (see :ref:`versioned-api-ref`), then :meth:`command` will + automactically add API versioning options to the given command. + Explicitly adding API versioning options in the command and + declaring an API version on the client is not supported. + .. versionchanged:: 3.6 Added ``session`` parameter. diff --git a/pymongo/message.py b/pymongo/message.py index e04d4c3b9..5df1c8707 100644 --- a/pymongo/message.py +++ b/pymongo/message.py @@ -307,6 +307,7 @@ class _Query(object): self.name = 'explain' cmd = SON([('explain', cmd)]) session = self.session + sock_info.add_server_api(cmd, session) if session: session._apply_to(cmd, False, self.read_preference) # Explain does not support readConcern. @@ -892,6 +893,7 @@ class _BulkWriteContext(object): self.compress = True if sock_info.compression_context else False self.op_type = op_type self.codec = codec + sock_info.add_server_api(command, session) def _batch_command(self, docs): namespace = self.db_name + '.$cmd' diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 2948b3321..bf439d1ec 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -498,8 +498,19 @@ class MongoClient(common.BaseObject): and automatically decrypt results. See :ref:`automatic-client-side-encryption` for an example. + | **Versioned API options:** + | (If not set explicitly, Versioned API will not be enabled.) + + - `server_api`: A + :class:`~pymongo.server_api.ServerApi` which configures this + client to use Versioned API. See :ref:`versioned-api-ref` for + details. + .. mongodoc:: connections + .. versionchanged:: 3.12 + Added the ``server_api`` keyword argument. + .. versionchanged:: 3.11 Added the following keyword arguments and URI options: diff --git a/pymongo/pool.py b/pymongo/pool.py index d39c9ed8f..9c0c4066d 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -60,6 +60,7 @@ from pymongo.monitoring import (ConnectionCheckOutFailedReason, from pymongo.network import (command, receive_message) from pymongo.read_preferences import ReadPreference +from pymongo.server_api import _add_to_command from pymongo.server_type import SERVER_TYPE from pymongo.socket_checker import SocketChecker # Always use our backport so we always have support for IP address matching @@ -311,7 +312,7 @@ class PoolOptions(object): '__ssl_context', '__ssl_match_hostname', '__socket_keepalive', '__event_listeners', '__appname', '__driver', '__metadata', '__compression_settings', '__max_connecting', - '__pause_enabled') + '__pause_enabled', '__server_api') def __init__(self, max_pool_size=MAX_POOL_SIZE, min_pool_size=MIN_POOL_SIZE, @@ -321,8 +322,7 @@ class PoolOptions(object): ssl_match_hostname=True, socket_keepalive=True, event_listeners=None, appname=None, driver=None, compression_settings=None, max_connecting=MAX_CONNECTING, - pause_enabled=True): - + pause_enabled=True, server_api=None): self.__max_pool_size = max_pool_size self.__min_pool_size = min_pool_size self.__max_idle_time_seconds = max_idle_time_seconds @@ -339,6 +339,7 @@ class PoolOptions(object): self.__compression_settings = compression_settings self.__max_connecting = max_connecting self.__pause_enabled = pause_enabled + self.__server_api = server_api self.__metadata = copy.deepcopy(_METADATA) if appname: self.__metadata['application'] = {'name': appname} @@ -495,6 +496,12 @@ class PoolOptions(object): """ return self.__metadata.copy() + @property + def server_api(self): + """A pymongo.server_api.ServerApi or None. + """ + return self.__server_api + def _negotiate_creds(all_credentials): """Return one credential that needs mechanism negotiation, if any. @@ -705,6 +712,7 @@ class SocketInfo(object): raise ConfigurationError( 'Must be connected to MongoDB 3.4+ to use a collation.') + self.add_server_api(spec, session) if session: session._apply_to(spec, retryable_write, read_preference) self.send_cluster_time(spec, session, client) @@ -894,6 +902,14 @@ class SocketInfo(object): if self.max_wire_version >= 6 and client: client._send_cluster_time(command, session) + def add_server_api(self, command, session): + """Add server_api parameters.""" + if (session and session.in_transaction and + not session._starting_transaction): + return + if self.opts.server_api: + _add_to_command(command, self.opts.server_api) + def update_last_checkin_time(self): self.last_checkin_time = _time() diff --git a/pymongo/server_api.py b/pymongo/server_api.py new file mode 100644 index 000000000..cf739659e --- /dev/null +++ b/pymongo/server_api.py @@ -0,0 +1,129 @@ +# Copyright 2020-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You +# may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Support for MongoDB Versioned API. + +.. _versioned-api-ref: + +MongoDB Versioned API +===================== + +To configure MongoDB Versioned API, pass the ``server_api`` keyword option to +:class:`~pymongo.mongo_client.MongoClient`:: + + from pymongo.mongo_client import MongoClient + from pymongo.server_api import ServerApi + + client = MongoClient(server_api=ServerApi('1')) + +Note that Versioned API requires MongoDB >=5.0. + +Strict Mode +``````````` + +When ``strict`` mode is configured, commands that are not supported in the +given :attr:`ServerApi.version` will fail. For example:: + + >>> client = MongoClient(server_api=ServerApi('1', strict=True)) + >>> client.test.command('count', 'test') + Traceback (most recent call last): + ... + pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError' + +Classes +======= +""" + + +class ServerApiVersion: + """An enum that defines values for :attr:`ServerApi.version`. + + .. versionadded:: 3.12 + """ + + V1 = "1" + """Server API version "1".""" + + +class ServerApi(object): + """MongoDB Versioned API.""" + def __init__(self, version, strict=None, deprecation_errors=None): + """Options to configure MongoDB Versioned API. + + :Parameters: + - `version`: The API version string. Must be one of the values in + :class:`ServerApiVersion`. + - `strict` (optional): Set to ``True`` to enable API strict mode. + Defaults to ``None`` which means "use the server's default". + - `deprecation_errors` (optional): Set to ``True`` to enable + deprecation errors. Defaults to ``None`` which means "use the + server's default". + + .. versionadded:: 3.12 + """ + if version != ServerApiVersion.V1: + raise ValueError("Unknown ServerApi version: %s" % (version,)) + if strict is not None and not isinstance(strict, bool): + raise TypeError( + "Wrong type for ServerApi strict, value must be an instance " + "of bool, not %s" % (type(strict),)) + if (deprecation_errors is not None and + not isinstance(deprecation_errors, bool)): + raise TypeError( + "Wrong type for ServerApi deprecation_errors, value must be " + "an instance of bool, not %s" % (type(deprecation_errors),)) + self._version = version + self._strict = strict + self._deprecation_errors = deprecation_errors + + @property + def version(self): + """The API version setting. + + This value is sent to the server in the "apiVersion" field. + """ + return self._version + + @property + def strict(self): + """The API strict mode setting. + + When set, this value is sent to the server in the "apiStrict" field. + """ + return self._strict + + @property + def deprecation_errors(self): + """The API deprecation errors setting. + + When set, this value is sent to the server in the + "apiDeprecationErrors" field. + """ + return self._deprecation_errors + + +def _add_to_command(cmd, server_api): + """Internal helper which adds API versioning options to a command. + + :Parameters: + - `cmd`: The command. + - `server_api` (optional): A :class:`ServerApi` or ``None``. + """ + if not server_api: + return + cmd['apiVersion'] = server_api.version + if server_api.strict is not None: + cmd['apiStrict'] = server_api.strict + if server_api.deprecation_errors is not None: + cmd['apiDeprecationErrors'] = server_api.deprecation_errors diff --git a/pymongo/topology.py b/pymongo/topology.py index db00280c5..2fc455efb 100644 --- a/pymongo/topology.py +++ b/pymongo/topology.py @@ -701,6 +701,7 @@ class Topology(object): appname=options.appname, driver=options.driver, pause_enabled=False, + server_api=options.server_api, ) return self._settings.pool_class(address, monitor_pool_options, diff --git a/test/__init__.py b/test/__init__.py index d9d362288..560dc1606 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -48,6 +48,7 @@ import pymongo.errors from bson.son import SON from pymongo import common, message from pymongo.common import partition_node +from pymongo.server_api import ServerApi from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs from test.version import Version @@ -90,6 +91,8 @@ if CA_PEM: TLS_OPTIONS['tlsCAFile'] = CA_PEM COMPRESSORS = os.environ.get("COMPRESSORS") +MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION") + def is_server_resolvable(): """Returns True if 'server' is resolvable.""" @@ -200,6 +203,7 @@ class ClientContext(object): self.version = Version(-1) # Needs to be comparable with Version self.auth_enabled = False self.test_commands_enabled = False + self.server_parameters = None self.is_mongos = False self.mongoses = [] self.is_rs = False @@ -212,9 +216,11 @@ class ClientContext(object): self.client = None self.conn_lock = threading.Lock() self.is_data_lake = False - if COMPRESSORS: self.default_client_options["compressors"] = COMPRESSORS + if MONGODB_API_VERSION: + server_api = ServerApi(MONGODB_API_VERSION) + self.default_client_options["server_api"] = server_api @property def ismaster(self): @@ -226,8 +232,7 @@ class ClientContext(object): timeout_ms = 10000 else: timeout_ms = 5000 - if COMPRESSORS: - kwargs["compressors"] = COMPRESSORS + kwargs.update(self.default_client_options) client = pymongo.MongoClient( host, port, serverSelectionTimeoutMS=timeout_ms, **kwargs) try: @@ -341,6 +346,8 @@ class ClientContext(object): self.nodes = set([(host, port)]) self.w = len(ismaster.get("hosts", [])) or 1 self.version = Version.from_client(self.client) + self.server_parameters = self.client.admin.command( + 'getParameter', '*') if 'enableTestCommands=1' in self.cmd_line['argv']: self.test_commands_enabled = True @@ -723,6 +730,12 @@ class ClientContext(object): "Transactions are not supported", func=func) + def require_no_api_version(self, func): + """Skip this test when testing with requireApiVersion.""" + return self._require(lambda: not MONGODB_API_VERSION, + "This test does not work with requireApiVersion", + func=func) + def mongos_seeds(self): return ','.join('%s:%s' % address for address in self.mongoses) @@ -766,6 +779,9 @@ def sanitize_cmd(cmd): cp.pop('$db', None) cp.pop('$readPreference', None) cp.pop('lsid', None) + if MONGODB_API_VERSION: + # Versioned api parameters + cp.pop('apiVersion', None) # OP_MSG encoding may move the payload type one field to the # end of the command. Do the same here. name = next(iter(cp)) diff --git a/test/test_bulk.py b/test/test_bulk.py index ec148a4ac..22e971d57 100644 --- a/test/test_bulk.py +++ b/test/test_bulk.py @@ -364,6 +364,7 @@ class BulkAuthorizationTestBase(BulkTestBase): @classmethod @client_context.require_auth + @client_context.require_no_api_version def setUpClass(cls): super(BulkAuthorizationTestBase, cls).setUpClass() diff --git a/test/test_pooling.py b/test/test_pooling.py index 024996a03..57338a965 100644 --- a/test/test_pooling.py +++ b/test/test_pooling.py @@ -176,6 +176,7 @@ class _TestPoolingBase(unittest.TestCase): pool_options = client_context.client._topology_settings.pool_options kwargs['ssl_context'] = pool_options.ssl_context kwargs['ssl_match_hostname'] = pool_options.ssl_match_hostname + kwargs['server_api'] = pool_options.server_api pool = Pool(pair, PoolOptions(*args, **kwargs)) pool.ready() return pool diff --git a/test/test_versioned_api.py b/test/test_versioned_api.py new file mode 100644 index 000000000..fc7c919af --- /dev/null +++ b/test/test_versioned_api.py @@ -0,0 +1,127 @@ +# Copyright 2020-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +sys.path[0:0] = [""] + +from pymongo.mongo_client import MongoClient +from pymongo.server_api import ServerApi, ServerApiVersion + +from test import client_context, IntegrationTest, unittest +from test.unified_format import generate_test_classes +from test.utils import OvertCommandListener, rs_or_single_client + + +TEST_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'versioned-api') + +# Generate unified tests. +globals().update(generate_test_classes(TEST_PATH, module=__name__)) + + +class TestServerApi(IntegrationTest): + def test_server_api_defaults(self): + api = ServerApi(ServerApiVersion.V1) + self.assertEqual(api.version, '1') + self.assertIsNone(api.strict) + self.assertIsNone(api.deprecation_errors) + + def test_server_api_explicit_false(self): + api = ServerApi('1', strict=False, deprecation_errors=False) + self.assertEqual(api.version, '1') + self.assertFalse(api.strict) + self.assertFalse(api.deprecation_errors) + + def test_server_api_strict(self): + api = ServerApi('1', strict=True, deprecation_errors=True) + self.assertEqual(api.version, '1') + self.assertTrue(api.strict) + self.assertTrue(api.deprecation_errors) + + def test_server_api_validation(self): + with self.assertRaises(ValueError): + ServerApi('2') + with self.assertRaises(TypeError): + ServerApi('1', strict='not-a-bool') + with self.assertRaises(TypeError): + ServerApi('1', deprecation_errors='not-a-bool') + with self.assertRaises(TypeError): + MongoClient(server_api='not-a-ServerApi') + + def assertServerApi(self, event): + self.assertIn('apiVersion', event.command) + self.assertEqual(event.command['apiVersion'], '1') + + def assertNoServerApi(self, event): + self.assertNotIn('apiVersion', event.command) + + def assertServerApiOnlyInFirstCommand(self, events): + self.assertServerApi(events[0]) + for event in events[1:]: + self.assertNoServerApi(event) + + @client_context.require_version_min(4, 7) + def test_command_options(self): + listener = OvertCommandListener() + client = rs_or_single_client(server_api=ServerApi('1'), + event_listeners=[listener]) + self.addCleanup(client.close) + coll = client.test.test + coll.insert_many([{} for _ in range(100)]) + self.addCleanup(coll.delete_many, {}) + list(coll.find(batch_size=25)) + client.admin.command('ping') + for event in listener.results['started']: + if event.command_name == 'getMore': + self.assertNoServerApi(event) + else: + self.assertServerApi(event) + + @client_context.require_version_min(4, 7) + @client_context.require_transactions + def test_command_options_txn(self): + listener = OvertCommandListener() + client = rs_or_single_client(server_api=ServerApi('1'), + event_listeners=[listener]) + self.addCleanup(client.close) + coll = client.test.test + coll.insert_many([{} for _ in range(100)]) + self.addCleanup(coll.delete_many, {}) + + listener.reset() + with client.start_session() as s, s.start_transaction(): + coll.insert_many([{} for _ in range(100)], session=s) + list(coll.find(batch_size=25, session=s)) + client.test.command('find', 'test', session=s) + self.assertServerApiOnlyInFirstCommand(listener.results['started']) + + listener.reset() + with client.start_session() as s, s.start_transaction(): + list(coll.find(batch_size=25, session=s)) + coll.insert_many([{} for _ in range(100)], session=s) + client.test.command('find', 'test', session=s) + self.assertServerApiOnlyInFirstCommand(listener.results['started']) + + listener.reset() + with client.start_session() as s, s.start_transaction(): + client.test.command('find', 'test', session=s) + list(coll.find(batch_size=25, session=s)) + coll.insert_many([{} for _ in range(100)], session=s) + self.assertServerApiOnlyInFirstCommand(listener.results['started']) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/unified_format.py b/test/unified_format.py index 3500b5634..04d22c201 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -45,6 +45,7 @@ from pymongo.monitoring import ( from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference from pymongo.results import BulkWriteResult +from pymongo.server_api import ServerApi from pymongo.write_concern import WriteConcern from test import client_context, unittest, IntegrationTest @@ -106,8 +107,17 @@ def is_run_on_requirement_satisfied(requirement): max_version_satisfied = Version.from_string( req_max_server_version) >= client_context.version + params_satisfied = True + params = requirement.get('serverParameters') + if params: + for param, val in params.items(): + if param not in client_context.server_parameters: + params_satisfied = False + elif client_context.server_parameters[param] != val: + params_satisfied = False + return (topology_satisfied and min_version_satisfied and - max_version_satisfied) + max_version_satisfied and params_satisfied) def parse_collection_or_database_options(options): @@ -200,6 +210,11 @@ class EntityMapUtil(object): if client_context.is_mongos and spec.get('useMultipleMongoses'): kwargs['h'] = client_context.mongos_seeds() kwargs.update(spec.get('uriOptions', {})) + server_api = spec.get('serverApi') + if server_api: + kwargs['server_api'] = ServerApi( + server_api['version'], strict=server_api.get('strict'), + deprecation_errors=server_api.get('deprecationErrors')) client = rs_or_single_client(**kwargs) self[spec['id']] = client self._test_class.addCleanup(client.close) @@ -478,6 +493,12 @@ class MatchEvaluatorUtil(object): command = spec.get('command') database_name = spec.get('databaseName') if command: + if actual.command_name == 'update': + # TODO: remove this once PYTHON-1744 is done. + # Add upsert and multi fields back into expectations. + for update in command['updates']: + update.setdefault('upsert', False) + update.setdefault('multi', False) self.match_result(command, actual.command) if database_name: self._test_class.assertEqual( @@ -503,7 +524,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest): Specification of the test suite being currently run is available as a class attribute ``TEST_SPEC``. """ - SCHEMA_VERSION = Version.from_string('1.0') + SCHEMA_VERSION = Version.from_string('1.1') @staticmethod def should_run_on(run_on_spec): diff --git a/test/versioned-api/crud-api-version-1-strict.json b/test/versioned-api/crud-api-version-1-strict.json new file mode 100644 index 000000000..5b4ccdb65 --- /dev/null +++ b/test/versioned-api/crud-api-version-1-strict.json @@ -0,0 +1,1073 @@ +{ + "description": "CRUD Api Version 1 (strict)", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1", + "strict": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "database": { + "id": "adminDatabase", + "client": "client", + "databaseName": "admin" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "aggregate on collection appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "aggregate on database appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "adminDatabase", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": 1, + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite appends declared API version", + "operations": [ + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 6, + "x": 66 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + }, + { + "updateMany": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 7 + } + } + }, + { + "replaceOne": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "ordered": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "countDocuments appends declared API version", + "operations": [ + { + "name": "countDocuments", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$gt": 11 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$match": { + "x": { + "$gt": 11 + } + } + }, + { + "$group": { + "_id": 1, + "n": { + "$sum": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "deleteMany appends declared API version", + "operations": [ + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "deleteOne appends declared API version", + "operations": [ + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 7 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "distinct appends declared API version", + "operations": [ + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "x", + "filter": {} + }, + "expectError": { + "isError": true, + "errorContains": "command distinct is not in API Version 1", + "errorCodeName": "APIStrictError" + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "test", + "key": "x", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "estimatedDocumentCount appends declared API version", + "skipReason": "DRIVERS-1437 count was removed from API version 1", + "operations": [ + { + "name": "estimatedDocumentCount", + "object": "collection", + "arguments": {} + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "count": "test", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "find command with declared API version appends to the command, but getMore does not", + "operations": [ + { + "name": "find", + "object": "collection", + "arguments": { + "filter": {}, + "sort": { + "_id": 1 + }, + "batchSize": 3 + }, + "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": "test", + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete appends declared API version", + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "remove": true, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace appends declared API version", + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate appends declared API version", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "insertMany appends declared API version", + "operations": [ + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "insertOne appends declared API version", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 6, + "x": 66 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "replaceOne appends declared API version", + "operations": [ + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "updateMany appends declared API version", + "operations": [ + { + "name": "updateMany", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + }, + { + "description": "updateOne appends declared API version", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/versioned-api/crud-api-version-1.json b/test/versioned-api/crud-api-version-1.json new file mode 100644 index 000000000..6584d8d2a --- /dev/null +++ b/test/versioned-api/crud-api-version-1.json @@ -0,0 +1,1067 @@ +{ + "description": "CRUD Api Version 1", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1", + "deprecationErrors": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "database": { + "id": "adminDatabase", + "client": "client", + "databaseName": "admin" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "aggregate on collection appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "aggregate on database appends declared API version", + "operations": [ + { + "name": "aggregate", + "object": "adminDatabase", + "arguments": { + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": 1, + "pipeline": [ + { + "$listLocalSessions": {} + }, + { + "$limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite appends declared API version", + "operations": [ + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 6, + "x": 66 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + }, + { + "updateMany": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 7 + } + } + }, + { + "replaceOne": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "ordered": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "countDocuments appends declared API version", + "operations": [ + { + "name": "countDocuments", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$gt": 11 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$match": { + "x": { + "$gt": 11 + } + } + }, + { + "$group": { + "_id": 1, + "n": { + "$sum": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "deleteMany appends declared API version", + "operations": [ + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "filter": { + "x": { + "$nin": [ + 24, + 34 + ] + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "x": { + "$nin": [ + 24, + 34 + ] + } + }, + "limit": 0 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "deleteOne appends declared API version", + "operations": [ + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 7 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 7 + }, + "limit": 1 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "distinct appends declared API version", + "operations": [ + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "x", + "filter": {} + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "test", + "key": "x", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "estimatedDocumentCount appends declared API version", + "operations": [ + { + "name": "estimatedDocumentCount", + "object": "collection", + "arguments": {} + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "count": "test", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "find command with declared API version appends to the command, but getMore does not", + "operations": [ + { + "name": "find", + "object": "collection", + "arguments": { + "filter": {}, + "sort": { + "_id": 1 + }, + "batchSize": 3 + }, + "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": "test", + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete appends declared API version", + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "remove": true, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace appends declared API version", + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate appends declared API version", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "insertMany appends declared API version", + "operations": [ + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "insertOne appends declared API version", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 6, + "x": 66 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "replaceOne appends declared API version", + "operations": [ + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "_id": 4, + "x": 44 + }, + "upsert": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "updateMany appends declared API version", + "operations": [ + { + "name": "updateMany", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, + { + "description": "updateOne appends declared API version", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 2 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + } + ] +} diff --git a/test/versioned-api/runcommand-helper-no-api-version-declared.json b/test/versioned-api/runcommand-helper-no-api-version-declared.json new file mode 100644 index 000000000..65c24ef46 --- /dev/null +++ b/test/versioned-api/runcommand-helper-no-api-version-declared.json @@ -0,0 +1,117 @@ +{ + "description": "RunCommand helper: No API version declared", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "serverParameters": { + "requireApiVersion": false + } + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + } + ], + "tests": [ + { + "description": "runCommand does not inspect or change the command document", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1, + "apiVersion": "server_will_never_support_this_api_version" + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "apiVersion": "server_will_never_support_this_api_version", + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + }, + "commandName": "ping", + "databaseName": "versioned-api-tests" + } + } + ] + } + ] + }, + { + "description": "runCommand does not prevent sending invalid API version declarations", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1, + "apiStrict": true + } + }, + "expectError": { + "isError": true, + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "apiVersion": { + "$$exists": false + }, + "apiStrict": true, + "apiDeprecationErrors": { + "$$exists": false + } + }, + "commandName": "ping", + "databaseName": "versioned-api-tests" + } + } + ] + } + ] + } + ] +} diff --git a/test/versioned-api/test-commands-deprecation-errors.json b/test/versioned-api/test-commands-deprecation-errors.json new file mode 100644 index 000000000..f4be168f6 --- /dev/null +++ b/test/versioned-api/test-commands-deprecation-errors.json @@ -0,0 +1,73 @@ +{ + "description": "Test commands: deprecation errors", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "serverParameters": { + "enableTestCommands": true, + "acceptAPIVersion2": true + } + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + } + ], + "tests": [ + { + "description": "Running a command that is deprecated raises a deprecation error", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "testDeprecationInVersion2", + "command": { + "testDeprecationInVersion2": 1, + "apiVersion": "2", + "apiDeprecationErrors": true + } + }, + "expectError": { + "isError": true, + "errorContains": "command testDeprecationInVersion2 is deprecated in API Version 2", + "errorCodeName": "APIDeprecationError" + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "testDeprecationInVersion2": 1, + "apiVersion": "2", + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + } + ] +} diff --git a/test/versioned-api/test-commands-strict-mode.json b/test/versioned-api/test-commands-strict-mode.json new file mode 100644 index 000000000..ebace4431 --- /dev/null +++ b/test/versioned-api/test-commands-strict-mode.json @@ -0,0 +1,74 @@ +{ + "description": "Test commands: strict mode", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "serverParameters": { + "enableTestCommands": true + } + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1", + "strict": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + } + ], + "tests": [ + { + "description": "Running a command that is not part of the versioned API results in an error", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "testVersion2", + "command": { + "testVersion2": 1 + } + }, + "expectError": { + "isError": true, + "errorContains": "command testVersion2 is not in API Version 1", + "errorCodeName": "APIStrictError" + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "testVersion2": 1, + "apiVersion": "1", + "apiStrict": true, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/versioned-api/transaction-handling.json b/test/versioned-api/transaction-handling.json new file mode 100644 index 000000000..313135c4b --- /dev/null +++ b/test/versioned-api/transaction-handling.json @@ -0,0 +1,232 @@ +{ + "description": "Transaction handling", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "4.7", + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "serverApi": { + "version": "1" + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "versioned-api-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + }, + { + "session": { + "id": "session", + "client": "client" + } + } + ], + "_yamlAnchors": { + "versions": [ + { + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + }, + { + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + ] + }, + "initialData": [ + { + "collectionName": "test", + "databaseName": "versioned-api-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "Only the first command in a transaction declares an API version", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded-replicaset" + ] + } + ], + "operations": [ + { + "name": "startTransaction", + "object": "session" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 6, + "x": 66 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 6 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session", + "document": { + "_id": 7, + "x": 77 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 7 + } + } + } + }, + { + "name": "commitTransaction", + "object": "session" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 6, + "x": 66 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "startTransaction": true, + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": { + "$$unsetOrMatches": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 7, + "x": 77 + } + ], + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session" + }, + "apiVersion": { + "$$exists": false + }, + "apiStrict": { + "$$exists": false + }, + "apiDeprecationErrors": { + "$$exists": false + } + } + } + } + ] + } + ] + } + ] +}