From d4b0f09d6d877a195bd4cade2ce6c7af8662e3b7 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 12:12:02 -0700 Subject: [PATCH 01/15] generate smoke test Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_smoke_applyOps.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_smoke_applyOps.py diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_smoke_applyOps.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_smoke_applyOps.py new file mode 100644 index 000000000..160491315 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_smoke_applyOps.py @@ -0,0 +1,35 @@ +""" +Smoke test for applyOps command. + +Tests basic applyOps command functionality by applying a single insert +oplog entry. +""" + +import pytest + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.smoke, pytest.mark.replica_set, pytest.mark.no_parallel] + + +def test_smoke_applyOps(collection): + """Test basic applyOps command behavior.""" + collection.insert_one({"_id": 0, "setup": True}) + + namespace = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "i", + "ns": namespace, + "o": {"_id": 1, "x": 1}, + } + ], + }, + ) + + expected = {"ok": 1.0} + assertSuccessPartial(result, expected, msg="Should support applyOps command") From 1bb82e23527d8504e24e51169fc4d53dc3730b81 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 14:48:54 -0700 Subject: [PATCH 02/15] initially generated tests Signed-off-by: Alina (Xi) Li --- .../replication/commands/applyOps/__init__.py | 0 .../test_applyOps_boolean_coercion.py | 144 +++++++ .../commands/applyOps/test_applyOps_core.py | 373 ++++++++++++++++++ .../test_applyOps_entry_validation.py | 217 ++++++++++ .../applyOps/test_applyOps_field_type.py | 88 +++++ .../applyOps/test_applyOps_multi_ops.py | 139 +++++++ .../applyOps/test_applyOps_options.py | 151 +++++++ .../applyOps/test_applyOps_rejected_params.py | 76 ++++ .../applyOps/test_applyOps_response.py | 69 ++++ documentdb_tests/framework/error_codes.py | 4 + 10 files changed, 1261 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/__init__.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py new file mode 100644 index 000000000..b6e8ccb01 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -0,0 +1,144 @@ +"""Tests for applyOps boolean coercion of allowAtomic and alwaysUpsert.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import ( + APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +@dataclass(frozen=True) +class BoolCoercionTest(BaseTestCase): + """Test case for boolean coercion tests.""" + + param_name: str | None = None + value: Any = None + + +# Property [allowAtomic Accepts All Types]: allowAtomic accepts any value +# without type rejection. All types are silently coerced or ignored. +ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[BoolCoercionTest] = [ + BoolCoercionTest( + f"allowatomic_{tid}", + param_name="allowAtomic", + value=val, + expected={"ok": 1.0}, + msg=f"applyOps should accept allowAtomic: {tid}", + ) + for tid, val in [ + ("bool_true", True), + ("bool_false", False), + ("int32_one", 1), + ("int32_zero", 0), + ("int64_one", Int64(1)), + ("int64_zero", Int64(0)), + ("double_one", 1.0), + ("double_zero", 0.0), + ("decimal128_one", Decimal128("1")), + ("decimal128_zero", Decimal128("0")), + ("string", "true"), + ("array", []), + ("object", {}), + ] +] + +# Property [alwaysUpsert No Longer Supported]: alwaysUpsert is rejected with +# a specific error for bool values, and TYPE_MISMATCH_ERROR for non-bool types. +ALWAYSUPSERT_BOOL_ERROR_TESTS: list[BoolCoercionTest] = [ + BoolCoercionTest( + "alwaysupsert_bool_true", + param_name="alwaysUpsert", + value=True, + error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + msg="applyOps should reject alwaysUpsert: true (no longer supported)", + ), +] + +# alwaysUpsert: false is accepted (equivalent to default behavior). +ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[BoolCoercionTest] = [ + BoolCoercionTest( + "alwaysupsert_bool_false", + param_name="alwaysUpsert", + value=False, + expected={"ok": 1.0}, + msg="applyOps should accept alwaysUpsert: false (equivalent to default)", + ), +] + +ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[BoolCoercionTest] = [ + BoolCoercionTest( + f"alwaysupsert_{tid}", + param_name="alwaysUpsert", + value=val, + error_code=TYPE_MISMATCH_ERROR, + msg=f"applyOps should reject alwaysUpsert: {tid} (wrong type)", + ) + for tid, val in [ + ("int32_one", 1), + ("int32_zero", 0), + ("int64_one", Int64(1)), + ("int64_zero", Int64(0)), + ("double_one", 1.0), + ("double_zero", 0.0), + ("decimal128_one", Decimal128("1")), + ("decimal128_zero", Decimal128("0")), + ] +] + +ALWAYSUPSERT_STRING_ERROR_TESTS: list[BoolCoercionTest] = [ + BoolCoercionTest( + f"alwaysupsert_{tid}", + param_name="alwaysUpsert", + value=val, + error_code=TYPE_MISMATCH_ERROR, + msg=f"applyOps should reject alwaysUpsert: {tid}", + ) + for tid, val in [ + ("string", "true"), + ("array", []), + ("object", {}), + ] +] + +ALWAYSUPSERT_ERROR_TESTS = ( + ALWAYSUPSERT_BOOL_ERROR_TESTS + + ALWAYSUPSERT_NONBOOL_ERROR_TESTS + + ALWAYSUPSERT_STRING_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALLOWATOMIC_COERCION_SUCCESS_TESTS)) +def test_applyOps_allowAtomic_accepted(collection, test): + """Test applyOps accepts all types for allowAtomic.""" + cmd = {"applyOps": [], test.param_name: test.value} + result = execute_admin_command(collection, cmd) + assertSuccessPartial(result, test.expected, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_FALSE_SUCCESS_TEST)) +def test_applyOps_alwaysUpsert_false_accepted(collection, test): + """Test applyOps accepts alwaysUpsert: false (equivalent to default).""" + cmd = {"applyOps": [], test.param_name: test.value} + result = execute_admin_command(collection, cmd) + assertSuccessPartial(result, test.expected, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_ERROR_TESTS)) +def test_applyOps_alwaysUpsert_rejected(collection, test): + """Test applyOps rejects alwaysUpsert (no longer supported).""" + cmd = {"applyOps": [], test.param_name: test.value} + result = execute_admin_command(collection, cmd) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py new file mode 100644 index 000000000..6e8082693 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -0,0 +1,373 @@ +"""Tests for applyOps command core behavior: operation types and basic functionality.""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +# --- Insert operations ("i") --- + + +def test_applyOps_insert_single_document(collection): + """Test applyOps inserts a single document.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert a single document") + + +def test_applyOps_insert_multiple_documents(collection): + """Test applyOps inserts multiple documents.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 1, "a": 1}}, + {"op": "i", "ns": ns, "o": {"_id": 2, "a": 2}}, + {"op": "i", "ns": ns, "o": {"_id": 3, "a": 3}}, + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert multiple documents") + + +def test_applyOps_insert_all_bson_types(collection): + """Test applyOps inserts a document containing all non-deprecated BSON types.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "i", + "ns": ns, + "o": { + "_id": 1, + "int32": 42, + "int64": Int64(123_456_789), + "double": 3.14, + "decimal128": Decimal128("1.23"), + "string": "hello", + "bool": True, + "date": datetime(2024, 1, 1, tzinfo=timezone.utc), + "null": None, + "object": {"nested": "value"}, + "array": [1, 2, 3], + "binary": Binary(b"\x00\x01"), + "objectid": ObjectId(), + "regex": Regex("abc", "i"), + "timestamp": Timestamp(1, 1), + "minkey": MinKey(), + "maxkey": MaxKey(), + }, + } + ] + }, + ) + assertSuccessPartial( + result, {"ok": 1.0}, msg="applyOps should insert a document with all BSON types" + ) + + +def test_applyOps_insert_nested_document(collection): + """Test applyOps inserts a document with nested objects and arrays.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "i", + "ns": ns, + "o": { + "_id": 1, + "a": {"b": {"c": {"d": 1}}}, + "arr": [[1, 2], [3, 4]], + "mixed": {"x": [{"y": 1}, {"y": 2}]}, + }, + } + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert a nested document") + + +def test_applyOps_insert_empty_document(collection): + """Test applyOps rejects inserting a document without _id.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {}}]}, + ) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="applyOps should reject insert of document without _id", + ) + + +def test_applyOps_insert_duplicate_id(collection): + """Test applyOps succeeds even when inserting a duplicate _id.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 2}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 1}, + msg="applyOps should succeed on duplicate _id insert", + ) + + +# --- Update operations ("u") --- + + +def test_applyOps_update_existing_document(collection): + """Test applyOps updates an existing document with replacement.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "u", + "ns": ns, + "o": {"_id": 1, "x": 2, "y": 3}, + "o2": {"_id": 1}, + } + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should update an existing document") + + +def test_applyOps_update_with_set_modifier(collection): + """Test applyOps updates with $v:2 diff format and $set modifier.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "u", + "ns": ns, + "o": {"$v": 2, "diff": {"u": {"x": 2}}}, + "o2": {"_id": 1}, + } + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should update with $v:2 diff format") + + +# --- Delete operations ("d") --- + + +def test_applyOps_delete_existing_document(collection): + """Test applyOps deletes an existing document.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should delete an existing document") + + +def test_applyOps_delete_nonexistent_document(collection): + """Test applyOps silently succeeds when deleting a non-existent document.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 999}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should succeed silently when deleting a non-existent document", + ) + + +# --- No-op operations ("n") --- + + +def test_applyOps_noop_operation(collection): + """Test applyOps accepts no-op operation.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept no-op operation") + + +def test_applyOps_noop_with_arbitrary_o(collection): + """Test applyOps accepts no-op with arbitrary o document.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {"msg": "test message"}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should accept no-op with arbitrary o document", + ) + + +# --- Command operations ("c") --- + + +def test_applyOps_command_create_collection(database_client, collection): + """Test applyOps creates a collection via command operation.""" + db_name = collection.database.name + coll_name = f"{collection.name}_applyops_create" + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "c", + "ns": f"{db_name}.$cmd", + "o": {"create": coll_name}, + } + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should create a collection via command operation", + ) + + +def test_applyOps_command_drop_collection(database_client, collection): + """Test applyOps drops a collection via command operation.""" + db_name = collection.database.name + coll_name = f"{collection.name}_applyops_drop" + database_client.create_collection(coll_name) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "c", + "ns": f"{db_name}.$cmd", + "o": {"drop": coll_name}, + } + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should drop a collection via command operation", + ) + + +# --- Empty operations array --- + + +def test_applyOps_empty_ops_array(collection): + """Test applyOps succeeds with an empty operations array.""" + result = execute_admin_command(collection, {"applyOps": []}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should succeed with an empty operations array", + ) + + +# --- Unrecognized fields --- + + +def test_applyOps_unrecognized_single_field(collection): + """Test applyOps ignores a single unrecognized field.""" + result = execute_admin_command(collection, {"applyOps": [], "unknownField": 1}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should ignore unrecognized fields", + ) + + +def test_applyOps_unrecognized_multiple_fields(collection): + """Test applyOps ignores multiple unrecognized fields.""" + result = execute_admin_command(collection, {"applyOps": [], "foo": 1, "bar": "baz"}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should ignore multiple unrecognized fields", + ) + + +def test_applyOps_unrecognized_dollar_prefix(collection): + """Test applyOps ignores dollar-prefixed unrecognized fields.""" + result = execute_admin_command(collection, {"applyOps": [], "$unknown": 1}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should ignore dollar-prefixed unrecognized fields", + ) + + +# --- Idempotent operations --- + + +def test_applyOps_idempotent_delete(collection): + """Test applying the same delete twice succeeds silently.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, + ) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should succeed silently when deleting an already-deleted document", + ) + + +def test_applyOps_idempotent_noop(collection): + """Test applying no-op twice succeeds.""" + ns = f"{collection.database.name}.{collection.name}" + execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should succeed when applying no-op twice", + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py new file mode 100644 index 000000000..4a80fed49 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -0,0 +1,217 @@ +"""Tests for applyOps operation entry validation.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + ILLEGAL_OPERATION_ERROR, + MISSING_FIELD_ERROR, + NO_SUCH_KEY_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +# --- Missing required fields --- + + +def test_applyOps_entry_missing_op(collection): + """Test applyOps rejects entry without op field.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"ns": ns, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="applyOps should reject entry without op field", + ) + + +def test_applyOps_entry_missing_ns(collection): + """Test applyOps rejects entry without ns field.""" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + ILLEGAL_OPERATION_ERROR, + msg="applyOps should reject entry without ns field", + ) + + +def test_applyOps_entry_missing_o(collection): + """Test applyOps rejects entry without o field.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns}]}, + ) + assertFailureCode( + result, + MISSING_FIELD_ERROR, + msg="applyOps should reject entry without o field", + ) + + +# --- Invalid op type --- + + +def test_applyOps_entry_invalid_op_type(collection): + """Test applyOps rejects invalid op type.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "x", "ns": ns, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + BAD_VALUE_ERROR, + msg="applyOps should reject invalid op type", + ) + + +def test_applyOps_entry_op_empty_string(collection): + """Test applyOps rejects empty string as op type.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "", "ns": ns, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + ILLEGAL_OPERATION_ERROR, + msg="applyOps should reject empty string op type", + ) + + +def test_applyOps_entry_op_non_string(collection): + """Test applyOps rejects non-string op type.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": 123, "ns": ns, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="applyOps should reject non-string op type", + ) + + +def test_applyOps_entry_op_null(collection): + """Test applyOps rejects null op type.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": None, "ns": ns, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="applyOps should reject null op type", + ) + + +# --- Invalid namespace --- + + +def test_applyOps_entry_ns_empty_string(collection): + """Test applyOps rejects empty string namespace.""" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + ILLEGAL_OPERATION_ERROR, + msg="applyOps should reject empty string namespace", + ) + + +def test_applyOps_entry_ns_non_string(collection): + """Test applyOps rejects non-string namespace.""" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + ILLEGAL_OPERATION_ERROR, + msg="applyOps should reject non-string namespace", + ) + + +def test_applyOps_entry_ns_null(collection): + """Test applyOps rejects null namespace.""" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, + ) + assertFailureCode( + result, + ILLEGAL_OPERATION_ERROR, + msg="applyOps should reject null namespace", + ) + + +# --- Invalid array entries --- + + +def test_applyOps_entry_non_object_int(collection): + """Test applyOps rejects integer entries in operations array.""" + result = execute_admin_command( + collection, + {"applyOps": [1, 2, 3]}, + ) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="applyOps should reject integer entries", + ) + + +def test_applyOps_entry_non_object_string(collection): + """Test applyOps rejects string entries in operations array.""" + result = execute_admin_command( + collection, + {"applyOps": ["insert"]}, + ) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="applyOps should reject string entries", + ) + + +def test_applyOps_entry_null(collection): + """Test applyOps rejects null entry in operations array.""" + result = execute_admin_command( + collection, + {"applyOps": [None]}, + ) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="applyOps should reject null entry", + ) + + +def test_applyOps_entry_empty_object(collection): + """Test applyOps rejects empty object entry.""" + result = execute_admin_command( + collection, + {"applyOps": [{}]}, + ) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="applyOps should reject empty object entry", + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py new file mode 100644 index 000000000..19db39dda --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py @@ -0,0 +1,88 @@ +"""Tests for applyOps command field type rejection.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone + +import pytest +from bson import ( + Binary, + Code, + Decimal128, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +@dataclass(frozen=True) +class ApplyOpsFieldTypeTest(BaseTestCase): + """Test case for applyOps field type rejection.""" + + value: object = None + + +# Property [Command Field Type Rejection]: the applyOps command field expects +# an array. All non-array BSON types are rejected. +APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[ApplyOpsFieldTypeTest] = [ + ApplyOpsFieldTypeTest( + f"field_type_{tid}", + value=val, + error_code=TYPE_MISMATCH_ERROR, + msg=f"applyOps should reject {tid} as command field value", + ) + for tid, val in [ + ("int32_positive", 1), + ("int32_zero", 0), + ("int32_negative", -1), + ("int64", Int64(1)), + ("int64_max", Int64(9_223_372_036_854_775_807)), + ("double", 1.0), + ("double_zero", 0.0), + ("double_negative", -1.0), + ("decimal128", Decimal128("1")), + ("bool_true", True), + ("bool_false", False), + ("nan", float("nan")), + ("infinity", float("inf")), + ("string", "test"), + ("string_empty", ""), + ("object_empty", {}), + ("object", {"key": "value"}), + ("binary", Binary(b"\x00\x01\x02")), + ("objectid", ObjectId()), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("regex", Regex(".*", "i")), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + [ + # Null also produces a type mismatch error. + ApplyOpsFieldTypeTest( + "field_type_null", + value=None, + error_code=TYPE_MISMATCH_ERROR, + msg="applyOps should reject null as command field value", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_FIELD_TYPE_ERROR_TESTS)) +def test_applyOps_field_type(collection, test): + """Test applyOps command field type rejection.""" + result = execute_admin_command(collection, {"applyOps": test.value}) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py new file mode 100644 index 000000000..1a44bb218 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -0,0 +1,139 @@ +"""Tests for applyOps multi-operation interactions.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +def test_applyOps_insert_then_update(collection): + """Test applyOps inserts a document and then updates it in the same batch.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, + { + "op": "u", + "ns": ns, + "o": {"_id": 1, "x": 2}, + "o2": {"_id": 1}, + }, + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert then update in one batch") + + +def test_applyOps_insert_then_delete(collection): + """Test applyOps inserts a document and then deletes it in the same batch.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, + {"op": "d", "ns": ns, "o": {"_id": 1}}, + ] + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert then delete in one batch") + + +def test_applyOps_update_then_delete(collection): + """Test applyOps updates a document and then deletes it in the same batch.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "u", + "ns": ns, + "o": {"_id": 1, "x": 2}, + "o2": {"_id": 1}, + }, + {"op": "d", "ns": ns, "o": {"_id": 1}}, + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should update then delete in one batch", + ) + + +def test_applyOps_multiple_inserts(collection): + """Test applyOps inserts multiple documents sequentially.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": -1, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": i, "x": i}} for i in range(5)]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 5}, + msg="applyOps should insert 5 documents", + ) + + +def test_applyOps_mixed_op_types(collection): + """Test applyOps with mixed insert, update, delete, and no-op operations.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, + { + "op": "u", + "ns": ns, + "o": {"_id": 1, "x": 2}, + "o2": {"_id": 1}, + }, + {"op": "n", "ns": ns, "o": {}}, + {"op": "d", "ns": ns, "o": {"_id": 1}}, + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should handle mixed operation types", + ) + + +def test_applyOps_cross_namespace(collection, database_client): + """Test applyOps operates across different namespaces in a single batch.""" + db_name = collection.database.name + coll1 = f"{collection.name}_ns1" + coll2 = f"{collection.name}_ns2" + database_client.create_collection(coll1) + database_client.create_collection(coll2) + ns1 = f"{db_name}.{coll1}" + ns2 = f"{db_name}.{coll2}" + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns1, "o": {"_id": 1, "src": "coll1"}}, + {"op": "i", "ns": ns2, "o": {"_id": 1, "src": "coll2"}}, + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should insert into multiple namespaces", + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py new file mode 100644 index 000000000..360a975d2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py @@ -0,0 +1,151 @@ +"""Tests for applyOps optional parameters: allowAtomic, alwaysUpsert, preCondition.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import ( + APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +# --- allowAtomic --- + + +def test_applyOps_allow_atomic_true(collection): + """Test applyOps accepts allowAtomic: true.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}], + "allowAtomic": True, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept allowAtomic: true") + + +def test_applyOps_allow_atomic_false(collection): + """Test applyOps accepts allowAtomic: false.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}], + "allowAtomic": False, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept allowAtomic: false") + + +def test_applyOps_allow_atomic_omitted(collection): + """Test applyOps defaults allowAtomic to true when omitted.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps should default allowAtomic to true", + ) + + +# --- alwaysUpsert (no longer supported) --- + + +def test_applyOps_always_upsert_rejected(collection): + """Test applyOps rejects alwaysUpsert option.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1}}], + "alwaysUpsert": True, + }, + ) + assertFailureCode( + result, + APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + msg="applyOps should reject alwaysUpsert (no longer supported)", + ) + + +# --- preCondition (no longer supported) --- + + +def test_applyOps_precondition_rejected(collection): + """Test applyOps rejects preCondition option.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1}}], + "preCondition": [], + }, + ) + assertFailureCode( + result, + APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + msg="applyOps should reject preCondition (no longer supported)", + ) + + +def test_applyOps_precondition_with_entries_rejected(collection): + """Test applyOps rejects preCondition with entries.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 10}) + result = execute_admin_command( + collection, + { + "applyOps": [ + { + "op": "u", + "ns": ns, + "o": {"_id": 1, "x": 20}, + "o2": {"_id": 1}, + } + ], + "preCondition": [{"ns": ns, "q": {"_id": 1}, "res": {"x": 10}}], + }, + ) + assertFailureCode( + result, + APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + msg="applyOps should reject preCondition with entries (no longer supported)", + ) + + +# --- allowAtomic effectiveness --- + + +def test_applyOps_atomic_false_partial_commit(collection): + """Test allowAtomic: false commits first op even if second fails.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 2, "x": 2}}, + {"op": "i", "ns": ns, "o": {"_id": 1, "x": 3}}, + ], + "allowAtomic": False, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="applyOps with allowAtomic: false should commit first op", + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py new file mode 100644 index 000000000..11bcb197d --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py @@ -0,0 +1,76 @@ +"""Tests for applyOps rejected parameters: prepare, partialTxn, count.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +@dataclass(frozen=True) +class ApplyOpsRejectedParamTest(BaseTestCase): + """Test case for applyOps rejected parameter tests.""" + + command: dict[str, Any] | None = None + + +# Property [Rejected Parameters]: prepare, partialTxn, and count fields are +# explicitly rejected by the applyOps command. +APPLYOPS_REJECTED_PARAM_TESTS: list[ApplyOpsRejectedParamTest] = [ + ApplyOpsRejectedParamTest( + "rejected_prepare_true", + command={"applyOps": [], "prepare": True}, + error_code=BAD_VALUE_ERROR, + msg="applyOps should reject prepare: true", + ), + ApplyOpsRejectedParamTest( + "rejected_prepare_false", + command={"applyOps": [], "prepare": False}, + error_code=BAD_VALUE_ERROR, + msg="applyOps should reject prepare: false", + ), + ApplyOpsRejectedParamTest( + "rejected_partial_txn_true", + command={"applyOps": [], "partialTxn": True}, + error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, + msg="applyOps should reject partialTxn: true", + ), + ApplyOpsRejectedParamTest( + "rejected_partial_txn_false", + command={"applyOps": [], "partialTxn": False}, + error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, + msg="applyOps should reject partialTxn: false", + ), + ApplyOpsRejectedParamTest( + "rejected_count_true", + command={"applyOps": [], "count": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="applyOps should reject count: true", + ), + ApplyOpsRejectedParamTest( + "rejected_count_false", + command={"applyOps": [], "count": False}, + error_code=TYPE_MISMATCH_ERROR, + msg="applyOps should reject count: false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_REJECTED_PARAM_TESTS)) +def test_applyOps_rejected_params(collection, test): + """Test applyOps rejects prepare, partialTxn, and count parameters.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py new file mode 100644 index 000000000..6bb392134 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py @@ -0,0 +1,69 @@ +"""Tests for applyOps response structure verification.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] + + +def test_applyOps_response_empty_ops(collection): + """Test applyOps response structure with empty ops array.""" + result = execute_admin_command(collection, {"applyOps": []}) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 0, "results": []}, + msg="applyOps should return applied: 0 and results: [] for empty ops", + ) + + +def test_applyOps_response_single_op(collection): + """Test applyOps response structure with a single insert operation.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 1, "results": [True]}, + msg="applyOps should return applied: 1 and results: [True] for single insert", + ) + + +def test_applyOps_response_multiple_ops(collection): + """Test applyOps response structure with multiple insert operations.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + result = execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns, "o": {"_id": 1, "a": 1}}, + {"op": "i", "ns": ns, "o": {"_id": 2, "a": 2}}, + ] + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 2, "results": [True, True]}, + msg="applyOps should return applied: 2 and results: [True, True] for two inserts", + ) + + +def test_applyOps_response_noop_op(collection): + """Test applyOps response structure with no-op operation.""" + ns = f"{collection.database.name}.{collection.name}" + result = execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "applied": 0}, + msg="applyOps should not count no-op in applied", + ) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 2375b9dcd..eba315ef3 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -6,6 +6,7 @@ BAD_VALUE_ERROR = 2 NO_SUCH_KEY_ERROR = 4 GRAPH_CONTAINS_CYCLE_ERROR = 5 +UNKNOWN_ERROR = 8 FAILED_TO_PARSE_ERROR = 9 UNAUTHORIZED_ERROR = 13 TYPE_MISMATCH_ERROR = 14 @@ -193,6 +194,7 @@ REGEX_MISSING_REGEX_ERROR = 31023 REGEX_UNKNOWN_FIELD_ERROR = 31024 KEY_FIELD_NULL_BYTE_ERROR = 31032 +PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR = 31056 OUT_OF_RANGE_CONVERSION_ERROR = 31109 UNSET_EMPTY_ARRAY_ERROR = 31119 UNSET_ARRAY_ELEMENT_TYPE_ERROR = 31120 @@ -490,6 +492,8 @@ ENCRYPTED_FIELD_UNSUPPORTED_TYPE_ERROR = 6338406 ENCRYPTED_FIELD_VIEW_TIMESERIES_ERROR = 6346401 ENCRYPTED_FIELD_CAPPED_ERROR = 6367301 +APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR = 6711600 +APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR = 6711601 ENCRYPTED_FIELD_RANGE_MIN_MAX_ERROR = 6720005 ENCRYPTED_FIELD_RANGE_TYPE_ERROR = 6775201 ENCRYPTED_FIELD_RANGE_MIN_MAX_TYPE_ERROR = 7018200 From 7e168ea55142cbda82323ba0e411e2a70d5a1252 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 15:19:58 -0700 Subject: [PATCH 03/15] replace ApplyOpsFieldTypeTest with CommandTestCase Signed-off-by: Alina (Xi) Li --- .../applyOps/test_applyOps_field_type.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py index 19db39dda..aaeb96bba 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py @@ -2,7 +2,6 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import datetime, timezone import pytest @@ -18,28 +17,23 @@ Timestamp, ) +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -@dataclass(frozen=True) -class ApplyOpsFieldTypeTest(BaseTestCase): - """Test case for applyOps field type rejection.""" - - value: object = None - - # Property [Command Field Type Rejection]: the applyOps command field expects # an array. All non-array BSON types are rejected. -APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[ApplyOpsFieldTypeTest] = [ - ApplyOpsFieldTypeTest( +APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"field_type_{tid}", - value=val, + command={"applyOps": val}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject {tid} as command field value", ) @@ -72,9 +66,9 @@ class ApplyOpsFieldTypeTest(BaseTestCase): ] ] + [ # Null also produces a type mismatch error. - ApplyOpsFieldTypeTest( + CommandTestCase( "field_type_null", - value=None, + command={"applyOps": None}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject null as command field value", ), @@ -84,5 +78,5 @@ class ApplyOpsFieldTypeTest(BaseTestCase): @pytest.mark.parametrize("test", pytest_params(APPLYOPS_FIELD_TYPE_ERROR_TESTS)) def test_applyOps_field_type(collection, test): """Test applyOps command field type rejection.""" - result = execute_admin_command(collection, {"applyOps": test.value}) + result = execute_admin_command(collection, test.command) assertFailureCode(result, test.error_code, msg=test.msg) From 770a9cfc37a2e1b296e1280f86fdf3f7729567ea Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 15:29:09 -0700 Subject: [PATCH 04/15] use CommandTestCase Signed-off-by: Alina (Xi) Li --- .../test_applyOps_boolean_coercion.py | 59 +++++++------------ .../applyOps/test_applyOps_rejected_params.py | 28 ++++----- 2 files changed, 31 insertions(+), 56 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py index b6e8ccb01..a56712d79 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -2,12 +2,12 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Any - import pytest from bson import Decimal128, Int64 +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial from documentdb_tests.framework.error_codes import ( APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, @@ -15,26 +15,16 @@ ) from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -@dataclass(frozen=True) -class BoolCoercionTest(BaseTestCase): - """Test case for boolean coercion tests.""" - - param_name: str | None = None - value: Any = None - - # Property [allowAtomic Accepts All Types]: allowAtomic accepts any value # without type rejection. All types are silently coerced or ignored. -ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[BoolCoercionTest] = [ - BoolCoercionTest( +ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"allowatomic_{tid}", - param_name="allowAtomic", - value=val, + command={"applyOps": [], "allowAtomic": val}, expected={"ok": 1.0}, msg=f"applyOps should accept allowAtomic: {tid}", ) @@ -57,32 +47,29 @@ class BoolCoercionTest(BaseTestCase): # Property [alwaysUpsert No Longer Supported]: alwaysUpsert is rejected with # a specific error for bool values, and TYPE_MISMATCH_ERROR for non-bool types. -ALWAYSUPSERT_BOOL_ERROR_TESTS: list[BoolCoercionTest] = [ - BoolCoercionTest( +ALWAYSUPSERT_BOOL_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( "alwaysupsert_bool_true", - param_name="alwaysUpsert", - value=True, + command={"applyOps": [], "alwaysUpsert": True}, error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, msg="applyOps should reject alwaysUpsert: true (no longer supported)", ), ] # alwaysUpsert: false is accepted (equivalent to default behavior). -ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[BoolCoercionTest] = [ - BoolCoercionTest( +ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[CommandTestCase] = [ + CommandTestCase( "alwaysupsert_bool_false", - param_name="alwaysUpsert", - value=False, + command={"applyOps": [], "alwaysUpsert": False}, expected={"ok": 1.0}, msg="applyOps should accept alwaysUpsert: false (equivalent to default)", ), ] -ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[BoolCoercionTest] = [ - BoolCoercionTest( +ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"alwaysupsert_{tid}", - param_name="alwaysUpsert", - value=val, + command={"applyOps": [], "alwaysUpsert": val}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject alwaysUpsert: {tid} (wrong type)", ) @@ -98,11 +85,10 @@ class BoolCoercionTest(BaseTestCase): ] ] -ALWAYSUPSERT_STRING_ERROR_TESTS: list[BoolCoercionTest] = [ - BoolCoercionTest( +ALWAYSUPSERT_STRING_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"alwaysupsert_{tid}", - param_name="alwaysUpsert", - value=val, + command={"applyOps": [], "alwaysUpsert": val}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject alwaysUpsert: {tid}", ) @@ -123,22 +109,19 @@ class BoolCoercionTest(BaseTestCase): @pytest.mark.parametrize("test", pytest_params(ALLOWATOMIC_COERCION_SUCCESS_TESTS)) def test_applyOps_allowAtomic_accepted(collection, test): """Test applyOps accepts all types for allowAtomic.""" - cmd = {"applyOps": [], test.param_name: test.value} - result = execute_admin_command(collection, cmd) + result = execute_admin_command(collection, test.command) assertSuccessPartial(result, test.expected, msg=test.msg) @pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_FALSE_SUCCESS_TEST)) def test_applyOps_alwaysUpsert_false_accepted(collection, test): """Test applyOps accepts alwaysUpsert: false (equivalent to default).""" - cmd = {"applyOps": [], test.param_name: test.value} - result = execute_admin_command(collection, cmd) + result = execute_admin_command(collection, test.command) assertSuccessPartial(result, test.expected, msg=test.msg) @pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_ERROR_TESTS)) def test_applyOps_alwaysUpsert_rejected(collection, test): """Test applyOps rejects alwaysUpsert (no longer supported).""" - cmd = {"applyOps": [], test.param_name: test.value} - result = execute_admin_command(collection, cmd) + result = execute_admin_command(collection, test.command) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py index 11bcb197d..02a5f79b6 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py @@ -2,11 +2,11 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Any - import pytest +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import ( BAD_VALUE_ERROR, @@ -15,52 +15,44 @@ ) from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -@dataclass(frozen=True) -class ApplyOpsRejectedParamTest(BaseTestCase): - """Test case for applyOps rejected parameter tests.""" - - command: dict[str, Any] | None = None - - # Property [Rejected Parameters]: prepare, partialTxn, and count fields are # explicitly rejected by the applyOps command. -APPLYOPS_REJECTED_PARAM_TESTS: list[ApplyOpsRejectedParamTest] = [ - ApplyOpsRejectedParamTest( +APPLYOPS_REJECTED_PARAM_TESTS: list[CommandTestCase] = [ + CommandTestCase( "rejected_prepare_true", command={"applyOps": [], "prepare": True}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: true", ), - ApplyOpsRejectedParamTest( + CommandTestCase( "rejected_prepare_false", command={"applyOps": [], "prepare": False}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: false", ), - ApplyOpsRejectedParamTest( + CommandTestCase( "rejected_partial_txn_true", command={"applyOps": [], "partialTxn": True}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: true", ), - ApplyOpsRejectedParamTest( + CommandTestCase( "rejected_partial_txn_false", command={"applyOps": [], "partialTxn": False}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: false", ), - ApplyOpsRejectedParamTest( + CommandTestCase( "rejected_count_true", command={"applyOps": [], "count": True}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject count: true", ), - ApplyOpsRejectedParamTest( + CommandTestCase( "rejected_count_false", command={"applyOps": [], "count": False}, error_code=TYPE_MISMATCH_ERROR, From e6aacb2ad566439d44c77dd24362890f03c15566 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 15:57:01 -0700 Subject: [PATCH 05/15] convert to use CommandTestCase Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_applyOps_core.py | 402 +++++++++--------- .../test_applyOps_entry_validation.py | 288 ++++++------- .../applyOps/test_applyOps_multi_ops.py | 160 +++---- .../applyOps/test_applyOps_options.py | 195 ++++----- .../applyOps/test_applyOps_response.py | 105 ++--- 5 files changed, 558 insertions(+), 592 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index 6e8082693..de00882a4 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -7,55 +7,54 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] # --- Insert operations ("i") --- - -def test_applyOps_insert_single_document(collection): - """Test applyOps inserts a single document.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert a single document") - - -def test_applyOps_insert_multiple_documents(collection): - """Test applyOps inserts multiple documents.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { +# Property [Insert Operations]: applyOps inserts documents into existing +# collections via the "i" op type. +APPLYOPS_INSERT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "insert_single_document", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should insert a single document", + ), + CommandTestCase( + "insert_multiple_documents", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 1, "a": 1}}, - {"op": "i", "ns": ns, "o": {"_id": 2, "a": 2}}, - {"op": "i", "ns": ns, "o": {"_id": 3, "a": 3}}, - ] + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "a": 1}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "a": 2}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 3, "a": 3}}, + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert multiple documents") - - -def test_applyOps_insert_all_bson_types(collection): - """Test applyOps inserts a document containing all non-deprecated BSON types.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { + expected={"ok": Eq(1.0)}, + msg="applyOps should insert multiple documents", + ), + CommandTestCase( + "insert_all_bson_types", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ { "op": "i", - "ns": ns, + "ns": ctx.namespace, "o": { "_id": 1, "int32": 42, @@ -76,25 +75,19 @@ def test_applyOps_insert_all_bson_types(collection): "maxkey": MaxKey(), }, } - ] + ], }, - ) - assertSuccessPartial( - result, {"ok": 1.0}, msg="applyOps should insert a document with all BSON types" - ) - - -def test_applyOps_insert_nested_document(collection): - """Test applyOps inserts a document with nested objects and arrays.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { + expected={"ok": Eq(1.0)}, + msg="applyOps should insert a document with all BSON types", + ), + CommandTestCase( + "insert_nested_document", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ { "op": "i", - "ns": ns, + "ns": ctx.namespace, "o": { "_id": 1, "a": {"b": {"c": {"d": 1}}}, @@ -102,142 +95,187 @@ def test_applyOps_insert_nested_document(collection): "mixed": {"x": [{"y": 1}, {"y": 2}]}, }, } - ] + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert a nested document") - - -def test_applyOps_insert_empty_document(collection): - """Test applyOps rejects inserting a document without _id.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {}}]}, - ) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, + expected={"ok": Eq(1.0)}, + msg="applyOps should insert a nested document", + ), + CommandTestCase( + "insert_empty_document_missing_id", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {}}], + }, + error_code=NO_SUCH_KEY_ERROR, msg="applyOps should reject insert of document without _id", - ) - - -def test_applyOps_insert_duplicate_id(collection): - """Test applyOps succeeds even when inserting a duplicate _id.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 2}}]}, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "applied": 1}, + ), + CommandTestCase( + "insert_duplicate_id", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 2}}], + }, + expected={"ok": Eq(1.0), "applied": Eq(1)}, msg="applyOps should succeed on duplicate _id insert", - ) + ), +] # --- Update operations ("u") --- - -def test_applyOps_update_existing_document(collection): - """Test applyOps updates an existing document with replacement.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - { +# Property [Update Operations]: applyOps updates documents via the "u" op type. +APPLYOPS_UPDATE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "update_existing_document", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { "applyOps": [ { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"_id": 1, "x": 2, "y": 3}, "o2": {"_id": 1}, } - ] + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should update an existing document") - - -def test_applyOps_update_with_set_modifier(collection): - """Test applyOps updates with $v:2 diff format and $set modifier.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - { + expected={"ok": Eq(1.0)}, + msg="applyOps should update an existing document", + ), + CommandTestCase( + "update_with_set_modifier", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { "applyOps": [ { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"$v": 2, "diff": {"u": {"x": 2}}}, "o2": {"_id": 1}, } - ] + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should update with $v:2 diff format") + expected={"ok": Eq(1.0)}, + msg="applyOps should update with $v:2 diff format", + ), +] # --- Delete operations ("d") --- +# Property [Delete Operations]: applyOps deletes documents via the "d" op type. +APPLYOPS_DELETE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "delete_existing_document", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [{"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should delete an existing document", + ), + CommandTestCase( + "delete_nonexistent_document", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "d", "ns": ctx.namespace, "o": {"_id": 999}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed silently when deleting a non-existent document", + ), +] -def test_applyOps_delete_existing_document(collection): - """Test applyOps deletes an existing document.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should delete an existing document") +# --- No-op operations ("n") --- -def test_applyOps_delete_nonexistent_document(collection): - """Test applyOps silently succeeds when deleting a non-existent document.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 999}}]}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="applyOps should succeed silently when deleting a non-existent document", - ) +# Property [No-op Operations]: applyOps accepts no-op entries via the "n" op type. +APPLYOPS_NOOP_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "noop_operation", + command=lambda ctx: { + "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should accept no-op operation", + ), + CommandTestCase( + "noop_with_arbitrary_o", + command=lambda ctx: { + "applyOps": [ + {"op": "n", "ns": ctx.namespace, "o": {"msg": "test message"}}, + ], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should accept no-op with arbitrary o document", + ), +] -# --- No-op operations ("n") --- +# --- Empty operations array --- +# Property [Empty Array]: applyOps succeeds with an empty operations array. +APPLYOPS_EMPTY_ARRAY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "empty_ops_array", + command={"applyOps": []}, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed with an empty operations array", + ), +] -def test_applyOps_noop_operation(collection): - """Test applyOps accepts no-op operation.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept no-op operation") +# --- Unrecognized fields --- -def test_applyOps_noop_with_arbitrary_o(collection): - """Test applyOps accepts no-op with arbitrary o document.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {"msg": "test message"}}]}, - ) - assertSuccessPartial( +# Property [Unrecognized Field Acceptance]: unknown fields are silently ignored. +APPLYOPS_UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unrecognized_single_field", + command={"applyOps": [], "unknownField": 1}, + expected={"ok": Eq(1.0)}, + msg="applyOps should ignore unrecognized fields", + ), + CommandTestCase( + "unrecognized_multiple_fields", + command={"applyOps": [], "foo": 1, "bar": "baz"}, + expected={"ok": Eq(1.0)}, + msg="applyOps should ignore multiple unrecognized fields", + ), + CommandTestCase( + "unrecognized_dollar_prefix", + command={"applyOps": [], "$unknown": 1}, + expected={"ok": Eq(1.0)}, + msg="applyOps should ignore dollar-prefixed unrecognized fields", + ), +] + + +APPLYOPS_CORE_TESTS: list[CommandTestCase] = ( + APPLYOPS_INSERT_TESTS + + APPLYOPS_UPDATE_TESTS + + APPLYOPS_DELETE_TESTS + + APPLYOPS_NOOP_TESTS + + APPLYOPS_EMPTY_ARRAY_TESTS + + APPLYOPS_UNRECOGNIZED_FIELD_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_CORE_TESTS)) +def test_applyOps_core(database_client, collection, test): + """Test applyOps command core behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( result, - {"ok": 1.0}, - msg="applyOps should accept no-op with arbitrary o document", + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, ) # --- Command operations ("c") --- +# These tests use database_client for cleanup and have custom setup, +# so they remain as standalone test functions. def test_applyOps_command_create_collection(database_client, collection): @@ -256,10 +294,11 @@ def test_applyOps_command_create_collection(database_client, collection): ] }, ) - assertSuccessPartial( + assertResult( result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should create a collection via command operation", + raw_res=True, ) @@ -280,60 +319,17 @@ def test_applyOps_command_drop_collection(database_client, collection): ] }, ) - assertSuccessPartial( + assertResult( result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should drop a collection via command operation", - ) - - -# --- Empty operations array --- - - -def test_applyOps_empty_ops_array(collection): - """Test applyOps succeeds with an empty operations array.""" - result = execute_admin_command(collection, {"applyOps": []}) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="applyOps should succeed with an empty operations array", - ) - - -# --- Unrecognized fields --- - - -def test_applyOps_unrecognized_single_field(collection): - """Test applyOps ignores a single unrecognized field.""" - result = execute_admin_command(collection, {"applyOps": [], "unknownField": 1}) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="applyOps should ignore unrecognized fields", - ) - - -def test_applyOps_unrecognized_multiple_fields(collection): - """Test applyOps ignores multiple unrecognized fields.""" - result = execute_admin_command(collection, {"applyOps": [], "foo": 1, "bar": "baz"}) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="applyOps should ignore multiple unrecognized fields", - ) - - -def test_applyOps_unrecognized_dollar_prefix(collection): - """Test applyOps ignores dollar-prefixed unrecognized fields.""" - result = execute_admin_command(collection, {"applyOps": [], "$unknown": 1}) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="applyOps should ignore dollar-prefixed unrecognized fields", + raw_res=True, ) # --- Idempotent operations --- +# These tests execute the same command twice, so they remain as standalone +# test functions. def test_applyOps_idempotent_delete(collection): @@ -348,10 +344,11 @@ def test_applyOps_idempotent_delete(collection): collection, {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, ) - assertSuccessPartial( + assertResult( result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should succeed silently when deleting an already-deleted document", + raw_res=True, ) @@ -366,8 +363,9 @@ def test_applyOps_idempotent_noop(collection): collection, {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, ) - assertSuccessPartial( + assertResult( result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should succeed when applying no-op twice", + raw_res=True, ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index 4a80fed49..670e85b5b 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -4,7 +4,11 @@ import pytest -from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( BAD_VALUE_ERROR, ILLEGAL_OPERATION_ERROR, @@ -13,205 +17,155 @@ TYPE_MISMATCH_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] # --- Missing required fields --- - -def test_applyOps_entry_missing_op(collection): - """Test applyOps rejects entry without op field.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"ns": ns, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, +# Property [Missing Required Fields]: applyOps rejects entries that omit +# required fields (op, ns, o). +APPLYOPS_MISSING_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "missing_op", + command=lambda ctx: { + "applyOps": [{"ns": ctx.namespace, "o": {"_id": 1}}], + }, + error_code=NO_SUCH_KEY_ERROR, msg="applyOps should reject entry without op field", - ) - - -def test_applyOps_entry_missing_ns(collection): - """Test applyOps rejects entry without ns field.""" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - ILLEGAL_OPERATION_ERROR, + ), + CommandTestCase( + "missing_ns", + command={"applyOps": [{"op": "i", "o": {"_id": 1}}]}, + error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject entry without ns field", - ) - - -def test_applyOps_entry_missing_o(collection): - """Test applyOps rejects entry without o field.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns}]}, - ) - assertFailureCode( - result, - MISSING_FIELD_ERROR, + ), + CommandTestCase( + "missing_o", + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace}], + }, + error_code=MISSING_FIELD_ERROR, msg="applyOps should reject entry without o field", - ) + ), +] # --- Invalid op type --- - -def test_applyOps_entry_invalid_op_type(collection): - """Test applyOps rejects invalid op type.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "x", "ns": ns, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - BAD_VALUE_ERROR, +# Property [Invalid Op Type]: applyOps rejects entries with invalid op values. +APPLYOPS_INVALID_OP_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "op_invalid_char", + command=lambda ctx: { + "applyOps": [{"op": "x", "ns": ctx.namespace, "o": {"_id": 1}}], + }, + error_code=BAD_VALUE_ERROR, msg="applyOps should reject invalid op type", - ) - - -def test_applyOps_entry_op_empty_string(collection): - """Test applyOps rejects empty string as op type.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "", "ns": ns, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - ILLEGAL_OPERATION_ERROR, + ), + CommandTestCase( + "op_empty_string", + command=lambda ctx: { + "applyOps": [{"op": "", "ns": ctx.namespace, "o": {"_id": 1}}], + }, + error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject empty string op type", - ) - - -def test_applyOps_entry_op_non_string(collection): - """Test applyOps rejects non-string op type.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": 123, "ns": ns, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, + ), + CommandTestCase( + "op_non_string", + command=lambda ctx: { + "applyOps": [{"op": 123, "ns": ctx.namespace, "o": {"_id": 1}}], + }, + error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject non-string op type", - ) - - -def test_applyOps_entry_op_null(collection): - """Test applyOps rejects null op type.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": None, "ns": ns, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, + ), + CommandTestCase( + "op_null", + command=lambda ctx: { + "applyOps": [{"op": None, "ns": ctx.namespace, "o": {"_id": 1}}], + }, + error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject null op type", - ) + ), +] # --- Invalid namespace --- - -def test_applyOps_entry_ns_empty_string(collection): - """Test applyOps rejects empty string namespace.""" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - ILLEGAL_OPERATION_ERROR, +# Property [Invalid Namespace]: applyOps rejects entries with invalid ns values. +APPLYOPS_INVALID_NS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "ns_empty_string", + command={"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, + error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject empty string namespace", - ) - - -def test_applyOps_entry_ns_non_string(collection): - """Test applyOps rejects non-string namespace.""" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - ILLEGAL_OPERATION_ERROR, + ), + CommandTestCase( + "ns_non_string", + command={"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, + error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject non-string namespace", - ) - - -def test_applyOps_entry_ns_null(collection): - """Test applyOps rejects null namespace.""" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, - ) - assertFailureCode( - result, - ILLEGAL_OPERATION_ERROR, + ), + CommandTestCase( + "ns_null", + command={"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, + error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject null namespace", - ) + ), +] # --- Invalid array entries --- - -def test_applyOps_entry_non_object_int(collection): - """Test applyOps rejects integer entries in operations array.""" - result = execute_admin_command( - collection, - {"applyOps": [1, 2, 3]}, - ) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, +# Property [Invalid Array Entries]: applyOps rejects non-object entries in +# the operations array. +APPLYOPS_INVALID_ENTRY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "entry_non_object_int", + command={"applyOps": [1, 2, 3]}, + error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject integer entries", - ) - - -def test_applyOps_entry_non_object_string(collection): - """Test applyOps rejects string entries in operations array.""" - result = execute_admin_command( - collection, - {"applyOps": ["insert"]}, - ) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, + ), + CommandTestCase( + "entry_non_object_string", + command={"applyOps": ["insert"]}, + error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject string entries", - ) + ), + CommandTestCase( + "entry_null", + command={"applyOps": [None]}, + error_code=TYPE_MISMATCH_ERROR, + msg="applyOps should reject null entry", + ), + CommandTestCase( + "entry_empty_object", + command={"applyOps": [{}]}, + error_code=NO_SUCH_KEY_ERROR, + msg="applyOps should reject empty object entry", + ), +] -def test_applyOps_entry_null(collection): - """Test applyOps rejects null entry in operations array.""" - result = execute_admin_command( - collection, - {"applyOps": [None]}, - ) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, - msg="applyOps should reject null entry", - ) +APPLYOPS_ENTRY_VALIDATION_TESTS: list[CommandTestCase] = ( + APPLYOPS_MISSING_FIELD_TESTS + + APPLYOPS_INVALID_OP_TESTS + + APPLYOPS_INVALID_NS_TESTS + + APPLYOPS_INVALID_ENTRY_TESTS +) -def test_applyOps_entry_empty_object(collection): - """Test applyOps rejects empty object entry.""" - result = execute_admin_command( - collection, - {"applyOps": [{}]}, - ) - assertFailureCode( +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_ENTRY_VALIDATION_TESTS)) +def test_applyOps_entry_validation(database_client, collection, test): + """Test applyOps operation entry validation.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( result, - NO_SUCH_KEY_ERROR, - msg="applyOps should reject empty object entry", + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 1a44bb218..e9f6c5805 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -4,116 +4,119 @@ import pytest -from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -def test_applyOps_insert_then_update(collection): - """Test applyOps inserts a document and then updates it in the same batch.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { +# Property [Multi-Operation Batches]: applyOps applies multiple operations +# in a single batch, combining insert, update, delete, and no-op ops. +APPLYOPS_MULTI_OPS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "insert_then_update", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}, { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"_id": 1, "x": 2}, "o2": {"_id": 1}, }, - ] + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert then update in one batch") - - -def test_applyOps_insert_then_delete(collection): - """Test applyOps inserts a document and then deletes it in the same batch.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { + expected={"ok": Eq(1.0)}, + msg="applyOps should insert then update in one batch", + ), + CommandTestCase( + "insert_then_delete", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, - {"op": "d", "ns": ns, "o": {"_id": 1}}, - ] + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}, + {"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}, + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should insert then delete in one batch") - - -def test_applyOps_update_then_delete(collection): - """Test applyOps updates a document and then deletes it in the same batch.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - { + expected={"ok": Eq(1.0)}, + msg="applyOps should insert then delete in one batch", + ), + CommandTestCase( + "update_then_delete", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { "applyOps": [ { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"_id": 1, "x": 2}, "o2": {"_id": 1}, }, - {"op": "d", "ns": ns, "o": {"_id": 1}}, - ] + {"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}, + ], }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should update then delete in one batch", - ) - - -def test_applyOps_multiple_inserts(collection): - """Test applyOps inserts multiple documents sequentially.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": -1, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": i, "x": i}} for i in range(5)]}, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "applied": 5}, + ), + CommandTestCase( + "multiple_inserts", + docs=[{"_id": -1, "setup": True}], + command=lambda ctx: { + "applyOps": [ + {"op": "i", "ns": ctx.namespace, "o": {"_id": i, "x": i}} for i in range(5) + ], + }, + expected={"ok": Eq(1.0), "applied": Eq(5)}, msg="applyOps should insert 5 documents", - ) - - -def test_applyOps_mixed_op_types(collection): - """Test applyOps with mixed insert, update, delete, and no-op operations.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { + ), + CommandTestCase( + "mixed_op_types", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}, { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"_id": 1, "x": 2}, "o2": {"_id": 1}, }, - {"op": "n", "ns": ns, "o": {}}, - {"op": "d", "ns": ns, "o": {"_id": 1}}, - ] + {"op": "n", "ns": ctx.namespace, "o": {}}, + {"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}, + ], }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should handle mixed operation types", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_MULTI_OPS_TESTS)) +def test_applyOps_multi_ops(database_client, collection, test): + """Test applyOps multi-operation interactions.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, ) +# --- Cross-namespace --- +# This test creates additional collections, so it remains standalone. + + def test_applyOps_cross_namespace(collection, database_client): """Test applyOps operates across different namespaces in a single batch.""" db_name = collection.database.name @@ -132,8 +135,9 @@ def test_applyOps_cross_namespace(collection, database_client): ] }, ) - assertSuccessPartial( + assertResult( result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps should insert into multiple namespaces", + raw_res=True, ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py index 360a975d2..ffbc704b7 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py @@ -4,148 +4,151 @@ import pytest -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] # --- allowAtomic --- - -def test_applyOps_allow_atomic_true(collection): - """Test applyOps accepts allowAtomic: true.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { - "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}], +# Property [allowAtomic]: applyOps accepts the allowAtomic option with +# boolean values and defaults to true when omitted. +APPLYOPS_ALLOW_ATOMIC_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "allow_atomic_true", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], "allowAtomic": True, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept allowAtomic: true") - - -def test_applyOps_allow_atomic_false(collection): - """Test applyOps accepts allowAtomic: false.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { - "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}], + expected={"ok": Eq(1.0)}, + msg="applyOps should accept allowAtomic: true", + ), + CommandTestCase( + "allow_atomic_false", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], "allowAtomic": False, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="applyOps should accept allowAtomic: false") - - -def test_applyOps_allow_atomic_omitted(collection): - """Test applyOps defaults allowAtomic to true when omitted.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, + msg="applyOps should accept allowAtomic: false", + ), + CommandTestCase( + "allow_atomic_omitted", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0)}, msg="applyOps should default allowAtomic to true", - ) + ), +] # --- alwaysUpsert (no longer supported) --- - -def test_applyOps_always_upsert_rejected(collection): - """Test applyOps rejects alwaysUpsert option.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { - "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1}}], +# Property [alwaysUpsert Rejection]: applyOps rejects the alwaysUpsert option. +APPLYOPS_ALWAYS_UPSERT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "always_upsert_rejected", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1}}], "alwaysUpsert": True, }, - ) - assertFailureCode( - result, - APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, msg="applyOps should reject alwaysUpsert (no longer supported)", - ) + ), +] # --- preCondition (no longer supported) --- - -def test_applyOps_precondition_rejected(collection): - """Test applyOps rejects preCondition option.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { - "applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1}}], +# Property [preCondition Rejection]: applyOps rejects the preCondition option. +APPLYOPS_PRECONDITION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "precondition_rejected", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1}}], "preCondition": [], }, - ) - assertFailureCode( - result, - APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, msg="applyOps should reject preCondition (no longer supported)", - ) - - -def test_applyOps_precondition_with_entries_rejected(collection): - """Test applyOps rejects preCondition with entries.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 10}) - result = execute_admin_command( - collection, - { + ), + CommandTestCase( + "precondition_with_entries_rejected", + docs=[{"_id": 1, "x": 10}], + command=lambda ctx: { "applyOps": [ { "op": "u", - "ns": ns, + "ns": ctx.namespace, "o": {"_id": 1, "x": 20}, "o2": {"_id": 1}, } ], - "preCondition": [{"ns": ns, "q": {"_id": 1}, "res": {"x": 10}}], + "preCondition": [ + {"ns": ctx.namespace, "q": {"_id": 1}, "res": {"x": 10}}, + ], }, - ) - assertFailureCode( - result, - APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, msg="applyOps should reject preCondition with entries (no longer supported)", - ) + ), +] # --- allowAtomic effectiveness --- - -def test_applyOps_atomic_false_partial_commit(collection): - """Test allowAtomic: false commits first op even if second fails.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - result = execute_admin_command( - collection, - { +# Property [allowAtomic Effectiveness]: allowAtomic: false allows partial +# commits when a later operation fails. +APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "atomic_false_partial_commit", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 2, "x": 2}}, - {"op": "i", "ns": ns, "o": {"_id": 1, "x": 3}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, ], "allowAtomic": False, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, + expected={"ok": Eq(1.0)}, msg="applyOps with allowAtomic: false should commit first op", + ), +] + + +APPLYOPS_OPTIONS_TESTS: list[CommandTestCase] = ( + APPLYOPS_ALLOW_ATOMIC_TESTS + + APPLYOPS_ALWAYS_UPSERT_TESTS + + APPLYOPS_PRECONDITION_TESTS + + APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_OPTIONS_TESTS)) +def test_applyOps_options(database_client, collection, test): + """Test applyOps optional parameters.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py index 6bb392134..7e3dfae50 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py @@ -4,66 +4,73 @@ import pytest -from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -def test_applyOps_response_empty_ops(collection): - """Test applyOps response structure with empty ops array.""" - result = execute_admin_command(collection, {"applyOps": []}) - assertSuccessPartial( - result, - {"ok": 1.0, "applied": 0, "results": []}, +# Property [Response Structure]: applyOps returns ok, applied, and results +# fields reflecting the operations performed. +APPLYOPS_RESPONSE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "response_empty_ops", + command={"applyOps": []}, + expected={"ok": Eq(1.0), "applied": Eq(0), "results": Eq([])}, msg="applyOps should return applied: 0 and results: [] for empty ops", - ) - - -def test_applyOps_response_single_op(collection): - """Test applyOps response structure with a single insert operation.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "applied": 1, "results": [True]}, + ), + CommandTestCase( + "response_single_op", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, msg="applyOps should return applied: 1 and results: [True] for single insert", - ) - - -def test_applyOps_response_multiple_ops(collection): - """Test applyOps response structure with multiple insert operations.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - result = execute_admin_command( - collection, - { + ), + CommandTestCase( + "response_multiple_ops", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { "applyOps": [ - {"op": "i", "ns": ns, "o": {"_id": 1, "a": 1}}, - {"op": "i", "ns": ns, "o": {"_id": 2, "a": 2}}, - ] + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "a": 1}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "a": 2}}, + ], + }, + expected={ + "ok": Eq(1.0), + "applied": Eq(2), + "results": Eq([True, True]), }, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "applied": 2, "results": [True, True]}, msg="applyOps should return applied: 2 and results: [True, True] for two inserts", - ) + ), + CommandTestCase( + "response_noop_op", + command=lambda ctx: { + "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], + }, + expected={"ok": Eq(1.0), "applied": Eq(0)}, + msg="applyOps should not count no-op in applied", + ), +] -def test_applyOps_response_noop_op(collection): - """Test applyOps response structure with no-op operation.""" - ns = f"{collection.database.name}.{collection.name}" - result = execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) - assertSuccessPartial( +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_RESPONSE_TESTS)) +def test_applyOps_response(database_client, collection, test): + """Test applyOps response structure verification.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( result, - {"ok": 1.0, "applied": 0}, - msg="applyOps should not count no-op in applied", + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, ) From 164ff513cddaeb6d84f13d122b6bb2dbc9413988 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 16:19:12 -0700 Subject: [PATCH 06/15] convert to use ReplicationTestCase Signed-off-by: Alina (Xi) Li --- .../test_applyOps_boolean_coercion.py | 24 +- .../commands/applyOps/test_applyOps_core.py | 245 +++++++++--------- .../test_applyOps_entry_validation.py | 49 ++-- .../applyOps/test_applyOps_field_type.py | 10 +- .../applyOps/test_applyOps_multi_ops.py | 79 +++--- .../applyOps/test_applyOps_options.py | 35 +-- .../applyOps/test_applyOps_rejected_params.py | 18 +- .../applyOps/test_applyOps_response.py | 21 +- .../system/replication/utils/__init__.py | 0 .../utils/replication_command_test_case.py | 25 ++ 10 files changed, 272 insertions(+), 234 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/system/replication/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py index a56712d79..b6a83ed93 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -5,8 +5,8 @@ import pytest from bson import Decimal128, Int64 -from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - CommandTestCase, +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial from documentdb_tests.framework.error_codes import ( @@ -21,8 +21,8 @@ # Property [allowAtomic Accepts All Types]: allowAtomic accepts any value # without type rejection. All types are silently coerced or ignored. -ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( f"allowatomic_{tid}", command={"applyOps": [], "allowAtomic": val}, expected={"ok": 1.0}, @@ -47,8 +47,8 @@ # Property [alwaysUpsert No Longer Supported]: alwaysUpsert is rejected with # a specific error for bool values, and TYPE_MISMATCH_ERROR for non-bool types. -ALWAYSUPSERT_BOOL_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( +ALWAYSUPSERT_BOOL_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "alwaysupsert_bool_true", command={"applyOps": [], "alwaysUpsert": True}, error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, @@ -57,8 +57,8 @@ ] # alwaysUpsert: false is accepted (equivalent to default behavior). -ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[CommandTestCase] = [ - CommandTestCase( +ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[ReplicationTestCase] = [ + ReplicationTestCase( "alwaysupsert_bool_false", command={"applyOps": [], "alwaysUpsert": False}, expected={"ok": 1.0}, @@ -66,8 +66,8 @@ ), ] -ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( +ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( f"alwaysupsert_{tid}", command={"applyOps": [], "alwaysUpsert": val}, error_code=TYPE_MISMATCH_ERROR, @@ -85,8 +85,8 @@ ] ] -ALWAYSUPSERT_STRING_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( +ALWAYSUPSERT_STRING_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( f"alwaysupsert_{tid}", command={"applyOps": [], "alwaysUpsert": val}, error_code=TYPE_MISMATCH_ERROR, diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index de00882a4..f337eb7f4 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -9,11 +9,13 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, - CommandTestCase, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -24,8 +26,8 @@ # Property [Insert Operations]: applyOps inserts documents into existing # collections via the "i" op type. -APPLYOPS_INSERT_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_INSERT_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "insert_single_document", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -34,7 +36,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert a single document", ), - CommandTestCase( + ReplicationTestCase( "insert_multiple_documents", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -47,7 +49,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert multiple documents", ), - CommandTestCase( + ReplicationTestCase( "insert_all_bson_types", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -80,7 +82,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert a document with all BSON types", ), - CommandTestCase( + ReplicationTestCase( "insert_nested_document", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -100,7 +102,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert a nested document", ), - CommandTestCase( + ReplicationTestCase( "insert_empty_document_missing_id", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -109,7 +111,7 @@ error_code=NO_SUCH_KEY_ERROR, msg="applyOps should reject insert of document without _id", ), - CommandTestCase( + ReplicationTestCase( "insert_duplicate_id", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -124,8 +126,8 @@ # --- Update operations ("u") --- # Property [Update Operations]: applyOps updates documents via the "u" op type. -APPLYOPS_UPDATE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_UPDATE_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "update_existing_document", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -141,7 +143,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should update an existing document", ), - CommandTestCase( + ReplicationTestCase( "update_with_set_modifier", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -163,8 +165,8 @@ # --- Delete operations ("d") --- # Property [Delete Operations]: applyOps deletes documents via the "d" op type. -APPLYOPS_DELETE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_DELETE_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "delete_existing_document", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -173,7 +175,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should delete an existing document", ), - CommandTestCase( + ReplicationTestCase( "delete_nonexistent_document", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -188,8 +190,8 @@ # --- No-op operations ("n") --- # Property [No-op Operations]: applyOps accepts no-op entries via the "n" op type. -APPLYOPS_NOOP_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_NOOP_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "noop_operation", command=lambda ctx: { "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], @@ -197,7 +199,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should accept no-op operation", ), - CommandTestCase( + ReplicationTestCase( "noop_with_arbitrary_o", command=lambda ctx: { "applyOps": [ @@ -210,11 +212,49 @@ ] +# --- Command operations ("c") --- + +# Property [Command Operations]: applyOps executes DDL commands via the "c" op type. +APPLYOPS_COMMAND_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "command_create_collection", + docs=[], + command=lambda ctx: { + "applyOps": [ + { + "op": "c", + "ns": f"{ctx.database}.$cmd", + "o": {"create": f"{ctx.collection}_applyops_create"}, + } + ], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should create a collection via command operation", + ), + ReplicationTestCase( + "command_drop_collection", + docs=[], + setup=lambda coll: coll.database.create_collection(f"{coll.name}_applyops_drop"), + command=lambda ctx: { + "applyOps": [ + { + "op": "c", + "ns": f"{ctx.database}.$cmd", + "o": {"drop": f"{ctx.collection}_applyops_drop"}, + } + ], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should drop a collection via command operation", + ), +] + + # --- Empty operations array --- # Property [Empty Array]: applyOps succeeds with an empty operations array. -APPLYOPS_EMPTY_ARRAY_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_EMPTY_ARRAY_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "empty_ops_array", command={"applyOps": []}, expected={"ok": Eq(1.0)}, @@ -226,20 +266,20 @@ # --- Unrecognized fields --- # Property [Unrecognized Field Acceptance]: unknown fields are silently ignored. -APPLYOPS_UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_UNRECOGNIZED_FIELD_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "unrecognized_single_field", command={"applyOps": [], "unknownField": 1}, expected={"ok": Eq(1.0)}, msg="applyOps should ignore unrecognized fields", ), - CommandTestCase( + ReplicationTestCase( "unrecognized_multiple_fields", command={"applyOps": [], "foo": 1, "bar": "baz"}, expected={"ok": Eq(1.0)}, msg="applyOps should ignore multiple unrecognized fields", ), - CommandTestCase( + ReplicationTestCase( "unrecognized_dollar_prefix", command={"applyOps": [], "$unknown": 1}, expected={"ok": Eq(1.0)}, @@ -248,13 +288,63 @@ ] -APPLYOPS_CORE_TESTS: list[CommandTestCase] = ( +# --- Idempotent operations --- + +# Property [Idempotent Operations]: applying the same operation twice succeeds. +APPLYOPS_IDEMPOTENT_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "idempotent_delete", + docs=[{"_id": 1, "x": 1}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "d", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1}, + } + ] + }, + ), + command=lambda ctx: { + "applyOps": [{"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed silently when deleting an already-deleted document", + ), + ReplicationTestCase( + "idempotent_noop", + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "n", + "ns": f"{coll.database.name}.{coll.name}", + "o": {}, + } + ] + }, + ), + command=lambda ctx: { + "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed when applying no-op twice", + ), +] + + +APPLYOPS_CORE_TESTS: list[ReplicationTestCase] = ( APPLYOPS_INSERT_TESTS + APPLYOPS_UPDATE_TESTS + APPLYOPS_DELETE_TESTS + APPLYOPS_NOOP_TESTS + + APPLYOPS_COMMAND_TESTS + APPLYOPS_EMPTY_ARRAY_TESTS + APPLYOPS_UNRECOGNIZED_FIELD_TESTS + + APPLYOPS_IDEMPOTENT_TESTS ) @@ -262,8 +352,13 @@ def test_applyOps_core(database_client, collection, test): """Test applyOps command core behavior.""" collection = test.prepare(database_client, collection) + if test.setup: + test.setup(collection) ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) + if test.use_admin: + result = execute_admin_command(collection, test.build_command(ctx)) + else: + result = execute_command(collection, test.build_command(ctx)) assertResult( result, expected=test.build_expected(ctx), @@ -271,101 +366,3 @@ def test_applyOps_core(database_client, collection, test): msg=test.msg, raw_res=True, ) - - -# --- Command operations ("c") --- -# These tests use database_client for cleanup and have custom setup, -# so they remain as standalone test functions. - - -def test_applyOps_command_create_collection(database_client, collection): - """Test applyOps creates a collection via command operation.""" - db_name = collection.database.name - coll_name = f"{collection.name}_applyops_create" - result = execute_admin_command( - collection, - { - "applyOps": [ - { - "op": "c", - "ns": f"{db_name}.$cmd", - "o": {"create": coll_name}, - } - ] - }, - ) - assertResult( - result, - expected={"ok": Eq(1.0)}, - msg="applyOps should create a collection via command operation", - raw_res=True, - ) - - -def test_applyOps_command_drop_collection(database_client, collection): - """Test applyOps drops a collection via command operation.""" - db_name = collection.database.name - coll_name = f"{collection.name}_applyops_drop" - database_client.create_collection(coll_name) - result = execute_admin_command( - collection, - { - "applyOps": [ - { - "op": "c", - "ns": f"{db_name}.$cmd", - "o": {"drop": coll_name}, - } - ] - }, - ) - assertResult( - result, - expected={"ok": Eq(1.0)}, - msg="applyOps should drop a collection via command operation", - raw_res=True, - ) - - -# --- Idempotent operations --- -# These tests execute the same command twice, so they remain as standalone -# test functions. - - -def test_applyOps_idempotent_delete(collection): - """Test applying the same delete twice succeeds silently.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, - ) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, - ) - assertResult( - result, - expected={"ok": Eq(1.0)}, - msg="applyOps should succeed silently when deleting an already-deleted document", - raw_res=True, - ) - - -def test_applyOps_idempotent_noop(collection): - """Test applying no-op twice succeeds.""" - ns = f"{collection.database.name}.{collection.name}" - execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) - assertResult( - result, - expected={"ok": Eq(1.0)}, - msg="applyOps should succeed when applying no-op twice", - raw_res=True, - ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index 670e85b5b..cf2eb6a50 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -6,7 +6,9 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, - CommandTestCase, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( @@ -16,7 +18,7 @@ NO_SUCH_KEY_ERROR, TYPE_MISMATCH_ERROR, ) -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] @@ -26,8 +28,8 @@ # Property [Missing Required Fields]: applyOps rejects entries that omit # required fields (op, ns, o). -APPLYOPS_MISSING_FIELD_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_MISSING_FIELD_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "missing_op", command=lambda ctx: { "applyOps": [{"ns": ctx.namespace, "o": {"_id": 1}}], @@ -35,13 +37,13 @@ error_code=NO_SUCH_KEY_ERROR, msg="applyOps should reject entry without op field", ), - CommandTestCase( + ReplicationTestCase( "missing_ns", command={"applyOps": [{"op": "i", "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject entry without ns field", ), - CommandTestCase( + ReplicationTestCase( "missing_o", command=lambda ctx: { "applyOps": [{"op": "i", "ns": ctx.namespace}], @@ -55,8 +57,8 @@ # --- Invalid op type --- # Property [Invalid Op Type]: applyOps rejects entries with invalid op values. -APPLYOPS_INVALID_OP_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_INVALID_OP_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "op_invalid_char", command=lambda ctx: { "applyOps": [{"op": "x", "ns": ctx.namespace, "o": {"_id": 1}}], @@ -64,7 +66,7 @@ error_code=BAD_VALUE_ERROR, msg="applyOps should reject invalid op type", ), - CommandTestCase( + ReplicationTestCase( "op_empty_string", command=lambda ctx: { "applyOps": [{"op": "", "ns": ctx.namespace, "o": {"_id": 1}}], @@ -72,7 +74,7 @@ error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject empty string op type", ), - CommandTestCase( + ReplicationTestCase( "op_non_string", command=lambda ctx: { "applyOps": [{"op": 123, "ns": ctx.namespace, "o": {"_id": 1}}], @@ -80,7 +82,7 @@ error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject non-string op type", ), - CommandTestCase( + ReplicationTestCase( "op_null", command=lambda ctx: { "applyOps": [{"op": None, "ns": ctx.namespace, "o": {"_id": 1}}], @@ -94,20 +96,20 @@ # --- Invalid namespace --- # Property [Invalid Namespace]: applyOps rejects entries with invalid ns values. -APPLYOPS_INVALID_NS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_INVALID_NS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "ns_empty_string", command={"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject empty string namespace", ), - CommandTestCase( + ReplicationTestCase( "ns_non_string", command={"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject non-string namespace", ), - CommandTestCase( + ReplicationTestCase( "ns_null", command={"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, @@ -120,26 +122,26 @@ # Property [Invalid Array Entries]: applyOps rejects non-object entries in # the operations array. -APPLYOPS_INVALID_ENTRY_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_INVALID_ENTRY_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "entry_non_object_int", command={"applyOps": [1, 2, 3]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject integer entries", ), - CommandTestCase( + ReplicationTestCase( "entry_non_object_string", command={"applyOps": ["insert"]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject string entries", ), - CommandTestCase( + ReplicationTestCase( "entry_null", command={"applyOps": [None]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject null entry", ), - CommandTestCase( + ReplicationTestCase( "entry_empty_object", command={"applyOps": [{}]}, error_code=NO_SUCH_KEY_ERROR, @@ -148,7 +150,7 @@ ] -APPLYOPS_ENTRY_VALIDATION_TESTS: list[CommandTestCase] = ( +APPLYOPS_ENTRY_VALIDATION_TESTS: list[ReplicationTestCase] = ( APPLYOPS_MISSING_FIELD_TESTS + APPLYOPS_INVALID_OP_TESTS + APPLYOPS_INVALID_NS_TESTS @@ -161,7 +163,10 @@ def test_applyOps_entry_validation(database_client, collection, test): """Test applyOps operation entry validation.""" collection = test.prepare(database_client, collection) ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) + if test.use_admin: + result = execute_admin_command(collection, test.build_command(ctx)) + else: + result = execute_command(collection, test.build_command(ctx)) assertResult( result, expected=test.build_expected(ctx), diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py index aaeb96bba..9d04f0284 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py @@ -17,8 +17,8 @@ Timestamp, ) -from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - CommandTestCase, +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR @@ -30,8 +30,8 @@ # Property [Command Field Type Rejection]: the applyOps command field expects # an array. All non-array BSON types are rejected. -APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( f"field_type_{tid}", command={"applyOps": val}, error_code=TYPE_MISMATCH_ERROR, @@ -66,7 +66,7 @@ ] ] + [ # Null also produces a type mismatch error. - CommandTestCase( + ReplicationTestCase( "field_type_null", command={"applyOps": None}, error_code=TYPE_MISMATCH_ERROR, diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index e9f6c5805..067bb90b1 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -6,10 +6,12 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, - CommandTestCase, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -18,8 +20,8 @@ # Property [Multi-Operation Batches]: applyOps applies multiple operations # in a single batch, combining insert, update, delete, and no-op ops. -APPLYOPS_MULTI_OPS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_MULTI_OPS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "insert_then_update", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -36,7 +38,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert then update in one batch", ), - CommandTestCase( + ReplicationTestCase( "insert_then_delete", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -48,7 +50,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert then delete in one batch", ), - CommandTestCase( + ReplicationTestCase( "update_then_delete", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -65,7 +67,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should update then delete in one batch", ), - CommandTestCase( + ReplicationTestCase( "multiple_inserts", docs=[{"_id": -1, "setup": True}], command=lambda ctx: { @@ -76,7 +78,7 @@ expected={"ok": Eq(1.0), "applied": Eq(5)}, msg="applyOps should insert 5 documents", ), - CommandTestCase( + ReplicationTestCase( "mixed_op_types", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -95,6 +97,30 @@ expected={"ok": Eq(1.0)}, msg="applyOps should handle mixed operation types", ), + ReplicationTestCase( + "cross_namespace", + docs=[], + setup=lambda coll: ( + coll.database.create_collection(f"{coll.name}_ns1"), + coll.database.create_collection(f"{coll.name}_ns2"), + ), + command=lambda ctx: { + "applyOps": [ + { + "op": "i", + "ns": f"{ctx.database}.{ctx.collection}_ns1", + "o": {"_id": 1, "src": "coll1"}, + }, + { + "op": "i", + "ns": f"{ctx.database}.{ctx.collection}_ns2", + "o": {"_id": 1, "src": "coll2"}, + }, + ], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should insert into multiple namespaces", + ), ] @@ -102,8 +128,13 @@ def test_applyOps_multi_ops(database_client, collection, test): """Test applyOps multi-operation interactions.""" collection = test.prepare(database_client, collection) + if test.setup: + test.setup(collection) ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) + if test.use_admin: + result = execute_admin_command(collection, test.build_command(ctx)) + else: + result = execute_command(collection, test.build_command(ctx)) assertResult( result, expected=test.build_expected(ctx), @@ -111,33 +142,3 @@ def test_applyOps_multi_ops(database_client, collection, test): msg=test.msg, raw_res=True, ) - - -# --- Cross-namespace --- -# This test creates additional collections, so it remains standalone. - - -def test_applyOps_cross_namespace(collection, database_client): - """Test applyOps operates across different namespaces in a single batch.""" - db_name = collection.database.name - coll1 = f"{collection.name}_ns1" - coll2 = f"{collection.name}_ns2" - database_client.create_collection(coll1) - database_client.create_collection(coll2) - ns1 = f"{db_name}.{coll1}" - ns2 = f"{db_name}.{coll2}" - result = execute_admin_command( - collection, - { - "applyOps": [ - {"op": "i", "ns": ns1, "o": {"_id": 1, "src": "coll1"}}, - {"op": "i", "ns": ns2, "o": {"_id": 1, "src": "coll2"}}, - ] - }, - ) - assertResult( - result, - expected={"ok": Eq(1.0)}, - msg="applyOps should insert into multiple namespaces", - raw_res=True, - ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py index ffbc704b7..dc668331d 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py @@ -6,14 +6,16 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, - CommandTestCase, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, ) -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -24,8 +26,8 @@ # Property [allowAtomic]: applyOps accepts the allowAtomic option with # boolean values and defaults to true when omitted. -APPLYOPS_ALLOW_ATOMIC_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_ALLOW_ATOMIC_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "allow_atomic_true", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -35,7 +37,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should accept allowAtomic: true", ), - CommandTestCase( + ReplicationTestCase( "allow_atomic_false", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -45,7 +47,7 @@ expected={"ok": Eq(1.0)}, msg="applyOps should accept allowAtomic: false", ), - CommandTestCase( + ReplicationTestCase( "allow_atomic_omitted", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -60,8 +62,8 @@ # --- alwaysUpsert (no longer supported) --- # Property [alwaysUpsert Rejection]: applyOps rejects the alwaysUpsert option. -APPLYOPS_ALWAYS_UPSERT_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_ALWAYS_UPSERT_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "always_upsert_rejected", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -77,8 +79,8 @@ # --- preCondition (no longer supported) --- # Property [preCondition Rejection]: applyOps rejects the preCondition option. -APPLYOPS_PRECONDITION_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_PRECONDITION_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "precondition_rejected", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -88,7 +90,7 @@ error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, msg="applyOps should reject preCondition (no longer supported)", ), - CommandTestCase( + ReplicationTestCase( "precondition_with_entries_rejected", docs=[{"_id": 1, "x": 10}], command=lambda ctx: { @@ -114,8 +116,8 @@ # Property [allowAtomic Effectiveness]: allowAtomic: false allows partial # commits when a later operation fails. -APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "atomic_false_partial_commit", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { @@ -131,7 +133,7 @@ ] -APPLYOPS_OPTIONS_TESTS: list[CommandTestCase] = ( +APPLYOPS_OPTIONS_TESTS: list[ReplicationTestCase] = ( APPLYOPS_ALLOW_ATOMIC_TESTS + APPLYOPS_ALWAYS_UPSERT_TESTS + APPLYOPS_PRECONDITION_TESTS @@ -144,7 +146,10 @@ def test_applyOps_options(database_client, collection, test): """Test applyOps optional parameters.""" collection = test.prepare(database_client, collection) ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) + if test.use_admin: + result = execute_admin_command(collection, test.build_command(ctx)) + else: + result = execute_command(collection, test.build_command(ctx)) assertResult( result, expected=test.build_expected(ctx), diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py index 02a5f79b6..4edb8b651 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py @@ -4,8 +4,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - CommandTestCase, +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import ( @@ -21,38 +21,38 @@ # Property [Rejected Parameters]: prepare, partialTxn, and count fields are # explicitly rejected by the applyOps command. -APPLYOPS_REJECTED_PARAM_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_REJECTED_PARAM_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "rejected_prepare_true", command={"applyOps": [], "prepare": True}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: true", ), - CommandTestCase( + ReplicationTestCase( "rejected_prepare_false", command={"applyOps": [], "prepare": False}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: false", ), - CommandTestCase( + ReplicationTestCase( "rejected_partial_txn_true", command={"applyOps": [], "partialTxn": True}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: true", ), - CommandTestCase( + ReplicationTestCase( "rejected_partial_txn_false", command={"applyOps": [], "partialTxn": False}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: false", ), - CommandTestCase( + ReplicationTestCase( "rejected_count_true", command={"applyOps": [], "count": True}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject count: true", ), - CommandTestCase( + ReplicationTestCase( "rejected_count_false", command={"applyOps": [], "count": False}, error_code=TYPE_MISMATCH_ERROR, diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py index 7e3dfae50..72bfd1495 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py @@ -6,10 +6,12 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, - CommandTestCase, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 + ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -18,14 +20,14 @@ # Property [Response Structure]: applyOps returns ok, applied, and results # fields reflecting the operations performed. -APPLYOPS_RESPONSE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +APPLYOPS_RESPONSE_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( "response_empty_ops", command={"applyOps": []}, expected={"ok": Eq(1.0), "applied": Eq(0), "results": Eq([])}, msg="applyOps should return applied: 0 and results: [] for empty ops", ), - CommandTestCase( + ReplicationTestCase( "response_single_op", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -34,7 +36,7 @@ expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, msg="applyOps should return applied: 1 and results: [True] for single insert", ), - CommandTestCase( + ReplicationTestCase( "response_multiple_ops", docs=[{"_id": 0, "setup": True}], command=lambda ctx: { @@ -50,7 +52,7 @@ }, msg="applyOps should return applied: 2 and results: [True, True] for two inserts", ), - CommandTestCase( + ReplicationTestCase( "response_noop_op", command=lambda ctx: { "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], @@ -66,7 +68,10 @@ def test_applyOps_response(database_client, collection, test): """Test applyOps response structure verification.""" collection = test.prepare(database_client, collection) ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) + if test.use_admin: + result = execute_admin_command(collection, test.build_command(ctx)) + else: + result = execute_command(collection, test.build_command(ctx)) assertResult( result, expected=test.build_expected(ctx), diff --git a/documentdb_tests/compatibility/tests/system/replication/utils/__init__.py b/documentdb_tests/compatibility/tests/system/replication/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py b/documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py new file mode 100644 index 000000000..5b93302e2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py @@ -0,0 +1,25 @@ +"""Shared test case for replication command tests.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) + + +@dataclass(frozen=True) +class ReplicationTestCase(CommandTestCase): + """Test case for replication command tests. + + Extends CommandTestCase with a ``use_admin`` flag that controls + whether the command is executed against the admin database. + + Attributes: + use_admin: If True (the default), execute against the admin + database via ``execute_admin_command``. If False, execute + against the test database via ``execute_command``. + """ + + use_admin: bool = True From b18ce1363eb513134434992dc8c62ce297f46bbd Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 17 Jun 2026 16:48:29 -0700 Subject: [PATCH 07/15] apply style guide Signed-off-by: Alina (Xi) Li --- .../test_applyOps_boolean_coercion.py | 24 ++-- .../commands/applyOps/test_applyOps_core.py | 127 +++++++----------- .../test_applyOps_entry_validation.py | 26 ++-- .../applyOps/test_applyOps_field_type.py | 12 +- .../applyOps/test_applyOps_multi_ops.py | 10 +- .../applyOps/test_applyOps_options.py | 10 +- .../applyOps/test_applyOps_rejected_params.py | 20 +-- .../applyOps/test_applyOps_response.py | 4 +- ..._test_case.py => replication_test_case.py} | 0 9 files changed, 99 insertions(+), 134 deletions(-) rename documentdb_tests/compatibility/tests/system/replication/utils/{replication_command_test_case.py => replication_test_case.py} (100%) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py index b6a83ed93..5151c2ee2 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -5,7 +5,10 @@ import pytest from bson import Decimal128, Int64 -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial @@ -24,7 +27,7 @@ ALLOWATOMIC_COERCION_SUCCESS_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( f"allowatomic_{tid}", - command={"applyOps": [], "allowAtomic": val}, + command=lambda ctx, v=val: {"applyOps": [], "allowAtomic": v}, expected={"ok": 1.0}, msg=f"applyOps should accept allowAtomic: {tid}", ) @@ -50,7 +53,7 @@ ALWAYSUPSERT_BOOL_ERROR_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "alwaysupsert_bool_true", - command={"applyOps": [], "alwaysUpsert": True}, + command=lambda ctx: {"applyOps": [], "alwaysUpsert": True}, error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, msg="applyOps should reject alwaysUpsert: true (no longer supported)", ), @@ -60,7 +63,7 @@ ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[ReplicationTestCase] = [ ReplicationTestCase( "alwaysupsert_bool_false", - command={"applyOps": [], "alwaysUpsert": False}, + command=lambda ctx: {"applyOps": [], "alwaysUpsert": False}, expected={"ok": 1.0}, msg="applyOps should accept alwaysUpsert: false (equivalent to default)", ), @@ -69,7 +72,7 @@ ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( f"alwaysupsert_{tid}", - command={"applyOps": [], "alwaysUpsert": val}, + command=lambda ctx, v=val: {"applyOps": [], "alwaysUpsert": v}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject alwaysUpsert: {tid} (wrong type)", ) @@ -88,7 +91,7 @@ ALWAYSUPSERT_STRING_ERROR_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( f"alwaysupsert_{tid}", - command={"applyOps": [], "alwaysUpsert": val}, + command=lambda ctx, v=val: {"applyOps": [], "alwaysUpsert": v}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject alwaysUpsert: {tid}", ) @@ -109,19 +112,22 @@ @pytest.mark.parametrize("test", pytest_params(ALLOWATOMIC_COERCION_SUCCESS_TESTS)) def test_applyOps_allowAtomic_accepted(collection, test): """Test applyOps accepts all types for allowAtomic.""" - result = execute_admin_command(collection, test.command) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.expected, msg=test.msg) @pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_FALSE_SUCCESS_TEST)) def test_applyOps_alwaysUpsert_false_accepted(collection, test): """Test applyOps accepts alwaysUpsert: false (equivalent to default).""" - result = execute_admin_command(collection, test.command) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.expected, msg=test.msg) @pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_ERROR_TESTS)) def test_applyOps_alwaysUpsert_rejected(collection, test): """Test applyOps rejects alwaysUpsert (no longer supported).""" - result = execute_admin_command(collection, test.command) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index f337eb7f4..e6239c41d 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -10,7 +10,7 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult @@ -18,12 +18,11 @@ from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.target_collection import SiblingCollection pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -# --- Insert operations ("i") --- - # Property [Insert Operations]: applyOps inserts documents into existing # collections via the "i" op type. APPLYOPS_INSERT_TESTS: list[ReplicationTestCase] = [ @@ -122,9 +121,6 @@ ), ] - -# --- Update operations ("u") --- - # Property [Update Operations]: applyOps updates documents via the "u" op type. APPLYOPS_UPDATE_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -161,9 +157,6 @@ ), ] - -# --- Delete operations ("d") --- - # Property [Delete Operations]: applyOps deletes documents via the "d" op type. APPLYOPS_DELETE_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -186,9 +179,6 @@ ), ] - -# --- No-op operations ("n") --- - # Property [No-op Operations]: applyOps accepts no-op entries via the "n" op type. APPLYOPS_NOOP_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -211,9 +201,6 @@ ), ] - -# --- Command operations ("c") --- - # Property [Command Operations]: applyOps executes DDL commands via the "c" op type. APPLYOPS_COMMAND_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -234,7 +221,7 @@ ReplicationTestCase( "command_drop_collection", docs=[], - setup=lambda coll: coll.database.create_collection(f"{coll.name}_applyops_drop"), + siblings=[SiblingCollection(suffix="_applyops_drop")], command=lambda ctx: { "applyOps": [ { @@ -249,93 +236,38 @@ ), ] - -# --- Empty operations array --- - # Property [Empty Array]: applyOps succeeds with an empty operations array. APPLYOPS_EMPTY_ARRAY_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "empty_ops_array", - command={"applyOps": []}, + command=lambda ctx: {"applyOps": []}, expected={"ok": Eq(1.0)}, msg="applyOps should succeed with an empty operations array", ), ] - -# --- Unrecognized fields --- - # Property [Unrecognized Field Acceptance]: unknown fields are silently ignored. APPLYOPS_UNRECOGNIZED_FIELD_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "unrecognized_single_field", - command={"applyOps": [], "unknownField": 1}, + command=lambda ctx: {"applyOps": [], "unknownField": 1}, expected={"ok": Eq(1.0)}, msg="applyOps should ignore unrecognized fields", ), ReplicationTestCase( "unrecognized_multiple_fields", - command={"applyOps": [], "foo": 1, "bar": "baz"}, + command=lambda ctx: {"applyOps": [], "foo": 1, "bar": "baz"}, expected={"ok": Eq(1.0)}, msg="applyOps should ignore multiple unrecognized fields", ), ReplicationTestCase( "unrecognized_dollar_prefix", - command={"applyOps": [], "$unknown": 1}, + command=lambda ctx: {"applyOps": [], "$unknown": 1}, expected={"ok": Eq(1.0)}, msg="applyOps should ignore dollar-prefixed unrecognized fields", ), ] - -# --- Idempotent operations --- - -# Property [Idempotent Operations]: applying the same operation twice succeeds. -APPLYOPS_IDEMPOTENT_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "idempotent_delete", - docs=[{"_id": 1, "x": 1}], - setup=lambda coll: execute_admin_command( - coll, - { - "applyOps": [ - { - "op": "d", - "ns": f"{coll.database.name}.{coll.name}", - "o": {"_id": 1}, - } - ] - }, - ), - command=lambda ctx: { - "applyOps": [{"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}], - }, - expected={"ok": Eq(1.0)}, - msg="applyOps should succeed silently when deleting an already-deleted document", - ), - ReplicationTestCase( - "idempotent_noop", - setup=lambda coll: execute_admin_command( - coll, - { - "applyOps": [ - { - "op": "n", - "ns": f"{coll.database.name}.{coll.name}", - "o": {}, - } - ] - }, - ), - command=lambda ctx: { - "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], - }, - expected={"ok": Eq(1.0)}, - msg="applyOps should succeed when applying no-op twice", - ), -] - - APPLYOPS_CORE_TESTS: list[ReplicationTestCase] = ( APPLYOPS_INSERT_TESTS + APPLYOPS_UPDATE_TESTS @@ -344,7 +276,6 @@ + APPLYOPS_COMMAND_TESTS + APPLYOPS_EMPTY_ARRAY_TESTS + APPLYOPS_UNRECOGNIZED_FIELD_TESTS - + APPLYOPS_IDEMPOTENT_TESTS ) @@ -352,8 +283,6 @@ def test_applyOps_core(database_client, collection, test): """Test applyOps command core behavior.""" collection = test.prepare(database_client, collection) - if test.setup: - test.setup(collection) ctx = CommandContext.from_collection(collection) if test.use_admin: result = execute_admin_command(collection, test.build_command(ctx)) @@ -366,3 +295,45 @@ def test_applyOps_core(database_client, collection, test): msg=test.msg, raw_res=True, ) + + +# Property [Idempotent Operations]: applying the same operation twice succeeds. + + +def test_applyOps_idempotent_delete(collection): + """Test applying the same delete twice succeeds silently.""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 1, "x": 1}) + execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, + ) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, + ) + assertResult( + result, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed silently when deleting an already-deleted document", + raw_res=True, + ) + + +def test_applyOps_idempotent_noop(collection): + """Test applying no-op twice succeeds.""" + ns = f"{collection.database.name}.{collection.name}" + execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, + ) + assertResult( + result, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed when applying no-op twice", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index cf2eb6a50..67e906a77 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -7,7 +7,7 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult @@ -24,8 +24,6 @@ pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -# --- Missing required fields --- - # Property [Missing Required Fields]: applyOps rejects entries that omit # required fields (op, ns, o). APPLYOPS_MISSING_FIELD_TESTS: list[ReplicationTestCase] = [ @@ -39,7 +37,7 @@ ), ReplicationTestCase( "missing_ns", - command={"applyOps": [{"op": "i", "o": {"_id": 1}}]}, + command=lambda ctx: {"applyOps": [{"op": "i", "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject entry without ns field", ), @@ -54,8 +52,6 @@ ] -# --- Invalid op type --- - # Property [Invalid Op Type]: applyOps rejects entries with invalid op values. APPLYOPS_INVALID_OP_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -93,57 +89,53 @@ ] -# --- Invalid namespace --- - # Property [Invalid Namespace]: applyOps rejects entries with invalid ns values. APPLYOPS_INVALID_NS_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "ns_empty_string", - command={"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, + command=lambda ctx: {"applyOps": [{"op": "i", "ns": "", "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject empty string namespace", ), ReplicationTestCase( "ns_non_string", - command={"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, + command=lambda ctx: {"applyOps": [{"op": "i", "ns": 123, "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject non-string namespace", ), ReplicationTestCase( "ns_null", - command={"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, + command=lambda ctx: {"applyOps": [{"op": "i", "ns": None, "o": {"_id": 1}}]}, error_code=ILLEGAL_OPERATION_ERROR, msg="applyOps should reject null namespace", ), ] -# --- Invalid array entries --- - # Property [Invalid Array Entries]: applyOps rejects non-object entries in # the operations array. APPLYOPS_INVALID_ENTRY_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "entry_non_object_int", - command={"applyOps": [1, 2, 3]}, + command=lambda ctx: {"applyOps": [1, 2, 3]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject integer entries", ), ReplicationTestCase( "entry_non_object_string", - command={"applyOps": ["insert"]}, + command=lambda ctx: {"applyOps": ["insert"]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject string entries", ), ReplicationTestCase( "entry_null", - command={"applyOps": [None]}, + command=lambda ctx: {"applyOps": [None]}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject null entry", ), ReplicationTestCase( "entry_empty_object", - command={"applyOps": [{}]}, + command=lambda ctx: {"applyOps": [{}]}, error_code=NO_SUCH_KEY_ERROR, msg="applyOps should reject empty object entry", ), diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py index 9d04f0284..0fe31421b 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_field_type.py @@ -17,7 +17,10 @@ Timestamp, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode @@ -33,7 +36,7 @@ APPLYOPS_FIELD_TYPE_ERROR_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( f"field_type_{tid}", - command={"applyOps": val}, + command=lambda ctx, v=val: {"applyOps": v}, error_code=TYPE_MISMATCH_ERROR, msg=f"applyOps should reject {tid} as command field value", ) @@ -68,7 +71,7 @@ # Null also produces a type mismatch error. ReplicationTestCase( "field_type_null", - command={"applyOps": None}, + command=lambda ctx: {"applyOps": None}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject null as command field value", ), @@ -78,5 +81,6 @@ @pytest.mark.parametrize("test", pytest_params(APPLYOPS_FIELD_TYPE_ERROR_TESTS)) def test_applyOps_field_type(collection, test): """Test applyOps command field type rejection.""" - result = execute_admin_command(collection, test.command) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 067bb90b1..3e581e6fb 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -7,13 +7,14 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.target_collection import SiblingCollection pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] @@ -100,10 +101,7 @@ ReplicationTestCase( "cross_namespace", docs=[], - setup=lambda coll: ( - coll.database.create_collection(f"{coll.name}_ns1"), - coll.database.create_collection(f"{coll.name}_ns2"), - ), + siblings=[SiblingCollection(suffix="_ns1"), SiblingCollection(suffix="_ns2")], command=lambda ctx: { "applyOps": [ { @@ -128,8 +126,6 @@ def test_applyOps_multi_ops(database_client, collection, test): """Test applyOps multi-operation interactions.""" collection = test.prepare(database_client, collection) - if test.setup: - test.setup(collection) ctx = CommandContext.from_collection(collection) if test.use_admin: result = execute_admin_command(collection, test.build_command(ctx)) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py index dc668331d..27095e99e 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py @@ -7,7 +7,7 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult @@ -22,8 +22,6 @@ pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] -# --- allowAtomic --- - # Property [allowAtomic]: applyOps accepts the allowAtomic option with # boolean values and defaults to true when omitted. APPLYOPS_ALLOW_ATOMIC_TESTS: list[ReplicationTestCase] = [ @@ -59,8 +57,6 @@ ] -# --- alwaysUpsert (no longer supported) --- - # Property [alwaysUpsert Rejection]: applyOps rejects the alwaysUpsert option. APPLYOPS_ALWAYS_UPSERT_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -76,8 +72,6 @@ ] -# --- preCondition (no longer supported) --- - # Property [preCondition Rejection]: applyOps rejects the preCondition option. APPLYOPS_PRECONDITION_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( @@ -112,8 +106,6 @@ ] -# --- allowAtomic effectiveness --- - # Property [allowAtomic Effectiveness]: allowAtomic: false allows partial # commits when a later operation fails. APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py index 4edb8b651..84ecf829f 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py @@ -4,7 +4,10 @@ import pytest -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, +) +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertFailureCode @@ -24,37 +27,37 @@ APPLYOPS_REJECTED_PARAM_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "rejected_prepare_true", - command={"applyOps": [], "prepare": True}, + command=lambda ctx: {"applyOps": [], "prepare": True}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: true", ), ReplicationTestCase( "rejected_prepare_false", - command={"applyOps": [], "prepare": False}, + command=lambda ctx: {"applyOps": [], "prepare": False}, error_code=BAD_VALUE_ERROR, msg="applyOps should reject prepare: false", ), ReplicationTestCase( "rejected_partial_txn_true", - command={"applyOps": [], "partialTxn": True}, + command=lambda ctx: {"applyOps": [], "partialTxn": True}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: true", ), ReplicationTestCase( "rejected_partial_txn_false", - command={"applyOps": [], "partialTxn": False}, + command=lambda ctx: {"applyOps": [], "partialTxn": False}, error_code=PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, msg="applyOps should reject partialTxn: false", ), ReplicationTestCase( "rejected_count_true", - command={"applyOps": [], "count": True}, + command=lambda ctx: {"applyOps": [], "count": True}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject count: true", ), ReplicationTestCase( "rejected_count_false", - command={"applyOps": [], "count": False}, + command=lambda ctx: {"applyOps": [], "count": False}, error_code=TYPE_MISMATCH_ERROR, msg="applyOps should reject count: false", ), @@ -64,5 +67,6 @@ @pytest.mark.parametrize("test", pytest_params(APPLYOPS_REJECTED_PARAM_TESTS)) def test_applyOps_rejected_params(collection, test): """Test applyOps rejects prepare, partialTxn, and count parameters.""" - result = execute_admin_command(collection, test.command) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py index 72bfd1495..ebc38484e 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py @@ -7,7 +7,7 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_command_test_case import ( # noqa: E501 +from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult @@ -23,7 +23,7 @@ APPLYOPS_RESPONSE_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "response_empty_ops", - command={"applyOps": []}, + command=lambda ctx: {"applyOps": []}, expected={"ok": Eq(1.0), "applied": Eq(0), "results": Eq([])}, msg="applyOps should return applied: 0 and results: [] for empty ops", ), diff --git a/documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py b/documentdb_tests/compatibility/tests/system/replication/utils/replication_test_case.py similarity index 100% rename from documentdb_tests/compatibility/tests/system/replication/utils/replication_command_test_case.py rename to documentdb_tests/compatibility/tests/system/replication/utils/replication_test_case.py From bd6457e50bdbe3cadfb66fcf3ede1cc2c7b6f5de Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 12:29:30 -0700 Subject: [PATCH 08/15] group success and error cases together Signed-off-by: Alina (Xi) Li --- .../test_applyOps_boolean_coercion.py | 87 ++-------- .../commands/applyOps/test_applyOps_core.py | 58 +++++-- .../test_applyOps_entry_validation.py | 9 ++ .../applyOps/test_applyOps_multi_ops.py | 66 +++++++- .../applyOps/test_applyOps_options.py | 151 ------------------ .../applyOps/test_applyOps_rejected_params.py | 86 +++++++++- .../applyOps/test_applyOps_response.py | 81 ---------- 7 files changed, 214 insertions(+), 324 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py delete mode 100644 documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py index 5151c2ee2..97b55fef1 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -1,4 +1,8 @@ -"""Tests for applyOps boolean coercion of allowAtomic and alwaysUpsert.""" +"""Tests for applyOps boolean coercion of allowAtomic and alwaysUpsert. + +Validates that allowAtomic accepts all BSON types and that +alwaysUpsert: false is accepted (equivalent to default behavior). +""" from __future__ import annotations @@ -11,11 +15,7 @@ from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial -from documentdb_tests.framework.error_codes import ( - APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, - TYPE_MISMATCH_ERROR, -) +from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params @@ -48,18 +48,8 @@ ] ] -# Property [alwaysUpsert No Longer Supported]: alwaysUpsert is rejected with -# a specific error for bool values, and TYPE_MISMATCH_ERROR for non-bool types. -ALWAYSUPSERT_BOOL_ERROR_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "alwaysupsert_bool_true", - command=lambda ctx: {"applyOps": [], "alwaysUpsert": True}, - error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, - msg="applyOps should reject alwaysUpsert: true (no longer supported)", - ), -] - -# alwaysUpsert: false is accepted (equivalent to default behavior). +# Property [alwaysUpsert False Accepted]: alwaysUpsert: false is accepted +# (equivalent to default behavior). ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[ReplicationTestCase] = [ ReplicationTestCase( "alwaysupsert_bool_false", @@ -69,65 +59,14 @@ ), ] -ALWAYSUPSERT_NONBOOL_ERROR_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - f"alwaysupsert_{tid}", - command=lambda ctx, v=val: {"applyOps": [], "alwaysUpsert": v}, - error_code=TYPE_MISMATCH_ERROR, - msg=f"applyOps should reject alwaysUpsert: {tid} (wrong type)", - ) - for tid, val in [ - ("int32_one", 1), - ("int32_zero", 0), - ("int64_one", Int64(1)), - ("int64_zero", Int64(0)), - ("double_one", 1.0), - ("double_zero", 0.0), - ("decimal128_one", Decimal128("1")), - ("decimal128_zero", Decimal128("0")), - ] -] - -ALWAYSUPSERT_STRING_ERROR_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - f"alwaysupsert_{tid}", - command=lambda ctx, v=val: {"applyOps": [], "alwaysUpsert": v}, - error_code=TYPE_MISMATCH_ERROR, - msg=f"applyOps should reject alwaysUpsert: {tid}", - ) - for tid, val in [ - ("string", "true"), - ("array", []), - ("object", {}), - ] -] - -ALWAYSUPSERT_ERROR_TESTS = ( - ALWAYSUPSERT_BOOL_ERROR_TESTS - + ALWAYSUPSERT_NONBOOL_ERROR_TESTS - + ALWAYSUPSERT_STRING_ERROR_TESTS +APPLYOPS_COERCION_SUCCESS_TESTS: list[ReplicationTestCase] = ( + ALLOWATOMIC_COERCION_SUCCESS_TESTS + ALWAYSUPSERT_FALSE_SUCCESS_TEST ) -@pytest.mark.parametrize("test", pytest_params(ALLOWATOMIC_COERCION_SUCCESS_TESTS)) -def test_applyOps_allowAtomic_accepted(collection, test): - """Test applyOps accepts all types for allowAtomic.""" +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_COERCION_SUCCESS_TESTS)) +def test_applyOps_boolean_coercion(collection, test): + """Test applyOps boolean coercion success cases.""" ctx = CommandContext.from_collection(collection) result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.expected, msg=test.msg) - - -@pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_FALSE_SUCCESS_TEST)) -def test_applyOps_alwaysUpsert_false_accepted(collection, test): - """Test applyOps accepts alwaysUpsert: false (equivalent to default).""" - ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) - assertSuccessPartial(result, test.expected, msg=test.msg) - - -@pytest.mark.parametrize("test", pytest_params(ALWAYSUPSERT_ERROR_TESTS)) -def test_applyOps_alwaysUpsert_rejected(collection, test): - """Test applyOps rejects alwaysUpsert (no longer supported).""" - ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) - assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index e6239c41d..3b7a5f773 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -1,4 +1,5 @@ -"""Tests for applyOps command core behavior: operation types and basic functionality.""" +"""Tests for applyOps command core behavior: operation types, response structure, +and basic functionality.""" from __future__ import annotations @@ -14,7 +15,6 @@ ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -101,15 +101,6 @@ expected={"ok": Eq(1.0)}, msg="applyOps should insert a nested document", ), - ReplicationTestCase( - "insert_empty_document_missing_id", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {}}], - }, - error_code=NO_SUCH_KEY_ERROR, - msg="applyOps should reject insert of document without _id", - ), ReplicationTestCase( "insert_duplicate_id", docs=[{"_id": 1, "x": 1}], @@ -268,6 +259,50 @@ ), ] +# Property [Response Structure]: applyOps returns ok, applied, and results +# fields reflecting the operations performed. +APPLYOPS_RESPONSE_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "response_empty_ops", + command=lambda ctx: {"applyOps": []}, + expected={"ok": Eq(1.0), "applied": Eq(0), "results": Eq([])}, + msg="applyOps should return applied: 0 and results: [] for empty ops", + ), + ReplicationTestCase( + "response_single_op", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, + msg="applyOps should return applied: 1 and results: [True] for single insert", + ), + ReplicationTestCase( + "response_multiple_ops", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [ + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "a": 1}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "a": 2}}, + ], + }, + expected={ + "ok": Eq(1.0), + "applied": Eq(2), + "results": Eq([True, True]), + }, + msg="applyOps should return applied: 2 and results: [True, True] for two inserts", + ), + ReplicationTestCase( + "response_noop_op", + command=lambda ctx: { + "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], + }, + expected={"ok": Eq(1.0), "applied": Eq(0)}, + msg="applyOps should not count no-op in applied", + ), +] + APPLYOPS_CORE_TESTS: list[ReplicationTestCase] = ( APPLYOPS_INSERT_TESTS + APPLYOPS_UPDATE_TESTS @@ -276,6 +311,7 @@ + APPLYOPS_COMMAND_TESTS + APPLYOPS_EMPTY_ARRAY_TESTS + APPLYOPS_UNRECOGNIZED_FIELD_TESTS + + APPLYOPS_RESPONSE_TESTS ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index 67e906a77..642ec12d6 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -49,6 +49,15 @@ error_code=MISSING_FIELD_ERROR, msg="applyOps should reject entry without o field", ), + ReplicationTestCase( + "insert_empty_document_missing_id", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {}}], + }, + error_code=NO_SUCH_KEY_ERROR, + msg="applyOps should reject insert of document without _id", + ), ] diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 3e581e6fb..8a20f2ef6 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -1,4 +1,8 @@ -"""Tests for applyOps multi-operation interactions.""" +"""Tests for applyOps multi-operation interactions and optional parameters. + +Validates multi-op batches, cross-namespace operations, and +allowAtomic behavior. +""" from __future__ import annotations @@ -121,10 +125,66 @@ ), ] +# Property [allowAtomic]: applyOps accepts the allowAtomic option with +# boolean values and defaults to true when omitted. +APPLYOPS_ALLOW_ATOMIC_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "allow_atomic_true", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + "allowAtomic": True, + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should accept allowAtomic: true", + ), + ReplicationTestCase( + "allow_atomic_false", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + "allowAtomic": False, + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should accept allowAtomic: false", + ), + ReplicationTestCase( + "allow_atomic_omitted", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should default allowAtomic to true", + ), +] + +# Property [allowAtomic Effectiveness]: allowAtomic: false allows partial +# commits when a later operation fails. +APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "atomic_false_partial_commit", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [ + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, + ], + "allowAtomic": False, + }, + expected={"ok": Eq(1.0)}, + msg="applyOps with allowAtomic: false should commit first op", + ), +] + +APPLYOPS_MULTI_OPS_AND_OPTIONS_TESTS: list[ReplicationTestCase] = ( + APPLYOPS_MULTI_OPS_TESTS + APPLYOPS_ALLOW_ATOMIC_TESTS + APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS +) + -@pytest.mark.parametrize("test", pytest_params(APPLYOPS_MULTI_OPS_TESTS)) +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_MULTI_OPS_AND_OPTIONS_TESTS)) def test_applyOps_multi_ops(database_client, collection, test): - """Test applyOps multi-operation interactions.""" + """Test applyOps multi-operation interactions and optional parameters.""" collection = test.prepare(database_client, collection) ctx = CommandContext.from_collection(collection) if test.use_admin: diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py deleted file mode 100644 index 27095e99e..000000000 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_options.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Tests for applyOps optional parameters: allowAtomic, alwaysUpsert, preCondition.""" - -from __future__ import annotations - -import pytest - -from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - CommandContext, -) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 - ReplicationTestCase, -) -from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.error_codes import ( - APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, - APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, -) -from documentdb_tests.framework.executor import execute_admin_command, execute_command -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq - -pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] - - -# Property [allowAtomic]: applyOps accepts the allowAtomic option with -# boolean values and defaults to true when omitted. -APPLYOPS_ALLOW_ATOMIC_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "allow_atomic_true", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], - "allowAtomic": True, - }, - expected={"ok": Eq(1.0)}, - msg="applyOps should accept allowAtomic: true", - ), - ReplicationTestCase( - "allow_atomic_false", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], - "allowAtomic": False, - }, - expected={"ok": Eq(1.0)}, - msg="applyOps should accept allowAtomic: false", - ), - ReplicationTestCase( - "allow_atomic_omitted", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], - }, - expected={"ok": Eq(1.0)}, - msg="applyOps should default allowAtomic to true", - ), -] - - -# Property [alwaysUpsert Rejection]: applyOps rejects the alwaysUpsert option. -APPLYOPS_ALWAYS_UPSERT_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "always_upsert_rejected", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1}}], - "alwaysUpsert": True, - }, - error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, - msg="applyOps should reject alwaysUpsert (no longer supported)", - ), -] - - -# Property [preCondition Rejection]: applyOps rejects the preCondition option. -APPLYOPS_PRECONDITION_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "precondition_rejected", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1}}], - "preCondition": [], - }, - error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, - msg="applyOps should reject preCondition (no longer supported)", - ), - ReplicationTestCase( - "precondition_with_entries_rejected", - docs=[{"_id": 1, "x": 10}], - command=lambda ctx: { - "applyOps": [ - { - "op": "u", - "ns": ctx.namespace, - "o": {"_id": 1, "x": 20}, - "o2": {"_id": 1}, - } - ], - "preCondition": [ - {"ns": ctx.namespace, "q": {"_id": 1}, "res": {"x": 10}}, - ], - }, - error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, - msg="applyOps should reject preCondition with entries (no longer supported)", - ), -] - - -# Property [allowAtomic Effectiveness]: allowAtomic: false allows partial -# commits when a later operation fails. -APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "atomic_false_partial_commit", - docs=[{"_id": 1, "x": 1}], - command=lambda ctx: { - "applyOps": [ - {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, - {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, - ], - "allowAtomic": False, - }, - expected={"ok": Eq(1.0)}, - msg="applyOps with allowAtomic: false should commit first op", - ), -] - - -APPLYOPS_OPTIONS_TESTS: list[ReplicationTestCase] = ( - APPLYOPS_ALLOW_ATOMIC_TESTS - + APPLYOPS_ALWAYS_UPSERT_TESTS - + APPLYOPS_PRECONDITION_TESTS - + APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS -) - - -@pytest.mark.parametrize("test", pytest_params(APPLYOPS_OPTIONS_TESTS)) -def test_applyOps_options(database_client, collection, test): - """Test applyOps optional parameters.""" - collection = test.prepare(database_client, collection) - ctx = CommandContext.from_collection(collection) - if test.use_admin: - result = execute_admin_command(collection, test.build_command(ctx)) - else: - result = execute_command(collection, test.build_command(ctx)) - assertResult( - result, - expected=test.build_expected(ctx), - error_code=test.error_code, - msg=test.msg, - raw_res=True, - ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py index 84ecf829f..338d513a0 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_rejected_params.py @@ -1,8 +1,13 @@ -"""Tests for applyOps rejected parameters: prepare, partialTxn, count.""" +"""Tests for applyOps rejected and unsupported parameters. + +Validates that applyOps rejects prepare, partialTxn, count, +alwaysUpsert, and preCondition parameters. +""" from __future__ import annotations import pytest +from bson import Decimal128, Int64 from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, @@ -12,6 +17,8 @@ ) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import ( + APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, BAD_VALUE_ERROR, PARTIAL_TRANSACTION_NOT_ALLOWED_ERROR, TYPE_MISMATCH_ERROR, @@ -63,10 +70,81 @@ ), ] +# Property [alwaysUpsert Rejection]: alwaysUpsert is no longer supported. +# Boolean true triggers a specific error; non-bool types trigger TYPE_MISMATCH_ERROR. +APPLYOPS_ALWAYSUPSERT_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "alwaysupsert_bool_true", + command=lambda ctx: {"applyOps": [], "alwaysUpsert": True}, + error_code=APPLYOPS_ALWAYS_UPSERT_NOT_SUPPORTED_ERROR, + msg="applyOps should reject alwaysUpsert: true (no longer supported)", + ), +] + [ + ReplicationTestCase( + f"alwaysupsert_{tid}", + command=lambda ctx, v=val: {"applyOps": [], "alwaysUpsert": v}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"applyOps should reject alwaysUpsert: {tid} (wrong type)", + ) + for tid, val in [ + ("int32_one", 1), + ("int32_zero", 0), + ("int64_one", Int64(1)), + ("int64_zero", Int64(0)), + ("double_one", 1.0), + ("double_zero", 0.0), + ("decimal128_one", Decimal128("1")), + ("decimal128_zero", Decimal128("0")), + ("string", "true"), + ("array", []), + ("object", {}), + ] +] + +# Property [preCondition Rejection]: applyOps rejects the preCondition option. +APPLYOPS_PRECONDITION_ERROR_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "precondition_rejected", + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1}}], + "preCondition": [], + }, + error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + msg="applyOps should reject preCondition (no longer supported)", + ), + ReplicationTestCase( + "precondition_with_entries_rejected", + docs=[{"_id": 1, "x": 10}], + command=lambda ctx: { + "applyOps": [ + { + "op": "u", + "ns": ctx.namespace, + "o": {"_id": 1, "x": 20}, + "o2": {"_id": 1}, + } + ], + "preCondition": [ + {"ns": ctx.namespace, "q": {"_id": 1}, "res": {"x": 10}}, + ], + }, + error_code=APPLYOPS_PRECONDITION_NOT_SUPPORTED_ERROR, + msg="applyOps should reject preCondition with entries (no longer supported)", + ), +] + +APPLYOPS_ALL_REJECTED_TESTS: list[ReplicationTestCase] = ( + APPLYOPS_REJECTED_PARAM_TESTS + + APPLYOPS_ALWAYSUPSERT_ERROR_TESTS + + APPLYOPS_PRECONDITION_ERROR_TESTS +) + -@pytest.mark.parametrize("test", pytest_params(APPLYOPS_REJECTED_PARAM_TESTS)) -def test_applyOps_rejected_params(collection, test): - """Test applyOps rejects prepare, partialTxn, and count parameters.""" +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_ALL_REJECTED_TESTS)) +def test_applyOps_rejected_params(database_client, collection, test): + """Test applyOps rejects unsupported parameters.""" + collection = test.prepare(database_client, collection) ctx = CommandContext.from_collection(collection) result = execute_admin_command(collection, test.build_command(ctx)) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py deleted file mode 100644 index ebc38484e..000000000 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_response.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Tests for applyOps response structure verification.""" - -from __future__ import annotations - -import pytest - -from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - CommandContext, -) -from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 - ReplicationTestCase, -) -from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.executor import execute_admin_command, execute_command -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq - -pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] - - -# Property [Response Structure]: applyOps returns ok, applied, and results -# fields reflecting the operations performed. -APPLYOPS_RESPONSE_TESTS: list[ReplicationTestCase] = [ - ReplicationTestCase( - "response_empty_ops", - command=lambda ctx: {"applyOps": []}, - expected={"ok": Eq(1.0), "applied": Eq(0), "results": Eq([])}, - msg="applyOps should return applied: 0 and results: [] for empty ops", - ), - ReplicationTestCase( - "response_single_op", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], - }, - expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, - msg="applyOps should return applied: 1 and results: [True] for single insert", - ), - ReplicationTestCase( - "response_multiple_ops", - docs=[{"_id": 0, "setup": True}], - command=lambda ctx: { - "applyOps": [ - {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "a": 1}}, - {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "a": 2}}, - ], - }, - expected={ - "ok": Eq(1.0), - "applied": Eq(2), - "results": Eq([True, True]), - }, - msg="applyOps should return applied: 2 and results: [True, True] for two inserts", - ), - ReplicationTestCase( - "response_noop_op", - command=lambda ctx: { - "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], - }, - expected={"ok": Eq(1.0), "applied": Eq(0)}, - msg="applyOps should not count no-op in applied", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(APPLYOPS_RESPONSE_TESTS)) -def test_applyOps_response(database_client, collection, test): - """Test applyOps response structure verification.""" - collection = test.prepare(database_client, collection) - ctx = CommandContext.from_collection(collection) - if test.use_admin: - result = execute_admin_command(collection, test.build_command(ctx)) - else: - result = execute_command(collection, test.build_command(ctx)) - assertResult( - result, - expected=test.build_expected(ctx), - error_code=test.error_code, - msg=test.msg, - raw_res=True, - ) From 280c76fe43fb9b38a26693cf4bb8838df3be86b2 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 12:51:28 -0700 Subject: [PATCH 09/15] add missing tests Signed-off-by: Alina (Xi) Li --- .../test_applyOps_boolean_coercion.py | 23 +++++-- .../commands/applyOps/test_applyOps_core.py | 32 +++++++++- .../test_applyOps_entry_validation.py | 61 +++++++++++++++++++ .../applyOps/test_applyOps_multi_ops.py | 18 +++++- 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py index 97b55fef1..b0cf820d7 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_boolean_coercion.py @@ -1,7 +1,8 @@ """Tests for applyOps boolean coercion of allowAtomic and alwaysUpsert. -Validates that allowAtomic accepts all BSON types and that -alwaysUpsert: false is accepted (equivalent to default behavior). +Validates that allowAtomic accepts all BSON types (including null, +negative zero, NaN, Infinity) and that alwaysUpsert: false/null are +accepted (equivalent to default behavior). """ from __future__ import annotations @@ -45,22 +46,32 @@ ("string", "true"), ("array", []), ("object", {}), + ("null", None), + ("neg_zero", -0.0), + ("nan", float("nan")), + ("infinity", float("inf")), ] ] -# Property [alwaysUpsert False Accepted]: alwaysUpsert: false is accepted -# (equivalent to default behavior). -ALWAYSUPSERT_FALSE_SUCCESS_TEST: list[ReplicationTestCase] = [ +# Property [alwaysUpsert Accepted Values]: alwaysUpsert: false and null are +# accepted (equivalent to default behavior). +ALWAYSUPSERT_ACCEPTED_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "alwaysupsert_bool_false", command=lambda ctx: {"applyOps": [], "alwaysUpsert": False}, expected={"ok": 1.0}, msg="applyOps should accept alwaysUpsert: false (equivalent to default)", ), + ReplicationTestCase( + "alwaysupsert_null", + command=lambda ctx: {"applyOps": [], "alwaysUpsert": None}, + expected={"ok": 1.0}, + msg="applyOps should accept alwaysUpsert: null (treated as omitted)", + ), ] APPLYOPS_COERCION_SUCCESS_TESTS: list[ReplicationTestCase] = ( - ALLOWATOMIC_COERCION_SUCCESS_TESTS + ALWAYSUPSERT_FALSE_SUCCESS_TEST + ALLOWATOMIC_COERCION_SUCCESS_TESTS + ALWAYSUPSERT_ACCEPTED_TESTS ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index 3b7a5f773..ceb20e384 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -18,7 +18,7 @@ from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq -from documentdb_tests.framework.target_collection import SiblingCollection +from documentdb_tests.framework.target_collection import CappedCollection, SiblingCollection pytestmark = [pytest.mark.replica_set, pytest.mark.no_parallel] @@ -110,6 +110,16 @@ expected={"ok": Eq(1.0), "applied": Eq(1)}, msg="applyOps should succeed on duplicate _id insert", ), + ReplicationTestCase( + "insert_into_capped_collection", + target_collection=CappedCollection(size=1_048_576), + docs=[{"_id": 0, "setup": True}], + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 1}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should insert into a capped collection", + ), ] # Property [Update Operations]: applyOps updates documents via the "u" op type. @@ -356,6 +366,26 @@ def test_applyOps_idempotent_delete(collection): ) +def test_applyOps_idempotent_insert(collection): + """Test applying the same insert twice succeeds (duplicate key is silently handled).""" + ns = f"{collection.database.name}.{collection.name}" + collection.insert_one({"_id": 0, "setup": True}) + execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, + ) + result = execute_admin_command( + collection, + {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 2}}]}, + ) + assertResult( + result, + expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, + msg="applyOps should succeed on duplicate _id insert (second apply)", + raw_res=True, + ) + + def test_applyOps_idempotent_noop(collection): """Test applying no-op twice succeeds.""" ns = f"{collection.database.name}.{collection.name}" diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index 642ec12d6..5d43bb88e 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -15,8 +15,10 @@ BAD_VALUE_ERROR, ILLEGAL_OPERATION_ERROR, MISSING_FIELD_ERROR, + NAMESPACE_NOT_FOUND_ERROR, NO_SUCH_KEY_ERROR, TYPE_MISMATCH_ERROR, + UNKNOWN_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params @@ -151,11 +153,70 @@ ] +# Property [Nonexistent Collection]: applyOps rejects insert/update operations +# targeting a namespace that does not exist. +APPLYOPS_NONEXISTENT_NS_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "insert_nonexistent_collection", + docs=[], + command=lambda ctx: { + "applyOps": [ + { + "op": "i", + "ns": f"{ctx.database}.{ctx.collection}_nonexistent", + "o": {"_id": 1, "x": 1}, + } + ], + }, + error_code=NAMESPACE_NOT_FOUND_ERROR, + msg="applyOps should reject insert into non-existent collection", + ), +] + +# Property [o2 Field Validation]: o2 (update query document) must contain +# _id and must match an existing document. +APPLYOPS_O2_FIELD_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "update_o2_no_match", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [ + { + "op": "u", + "ns": ctx.namespace, + "o": {"_id": 999, "x": 2}, + "o2": {"_id": 999}, + } + ], + }, + error_code=UNKNOWN_ERROR, + msg="applyOps should error when o2 does not match any document", + ), + ReplicationTestCase( + "update_o2_empty", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [ + { + "op": "u", + "ns": ctx.namespace, + "o": {"_id": 1, "x": 99}, + "o2": {}, + } + ], + }, + error_code=NO_SUCH_KEY_ERROR, + msg="applyOps should reject update with empty o2 (missing _id)", + ), +] + APPLYOPS_ENTRY_VALIDATION_TESTS: list[ReplicationTestCase] = ( APPLYOPS_MISSING_FIELD_TESTS + APPLYOPS_INVALID_OP_TESTS + APPLYOPS_INVALID_NS_TESTS + APPLYOPS_INVALID_ENTRY_TESTS + + APPLYOPS_NONEXISTENT_NS_TESTS + + APPLYOPS_O2_FIELD_TESTS ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 8a20f2ef6..52e919480 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -159,8 +159,9 @@ ), ] -# Property [allowAtomic Effectiveness]: allowAtomic: false allows partial -# commits when a later operation fails. +# Property [allowAtomic Effectiveness]: allowAtomic controls whether +# multi-op application is atomic. With false, partial commits are allowed. +# With true (default), duplicate key inserts still succeed (applied: 2). APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "atomic_false_partial_commit", @@ -175,6 +176,19 @@ expected={"ok": Eq(1.0)}, msg="applyOps with allowAtomic: false should commit first op", ), + ReplicationTestCase( + "atomic_true_dup_key", + docs=[{"_id": 1, "x": 1}], + command=lambda ctx: { + "applyOps": [ + {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, + {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, + ], + "allowAtomic": True, + }, + expected={"ok": Eq(1.0), "applied": Eq(2)}, + msg="applyOps with allowAtomic: true should succeed on duplicate key", + ), ] APPLYOPS_MULTI_OPS_AND_OPTIONS_TESTS: list[ReplicationTestCase] = ( From 723920da45afb168d458f4fc654754c096f71b3b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 12:55:27 -0700 Subject: [PATCH 10/15] remove duplicate Signed-off-by: Alina (Xi) Li --- .../applyOps/test_applyOps_multi_ops.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 52e919480..8a20f2ef6 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -159,9 +159,8 @@ ), ] -# Property [allowAtomic Effectiveness]: allowAtomic controls whether -# multi-op application is atomic. With false, partial commits are allowed. -# With true (default), duplicate key inserts still succeed (applied: 2). +# Property [allowAtomic Effectiveness]: allowAtomic: false allows partial +# commits when a later operation fails. APPLYOPS_ATOMIC_EFFECTIVENESS_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( "atomic_false_partial_commit", @@ -176,19 +175,6 @@ expected={"ok": Eq(1.0)}, msg="applyOps with allowAtomic: false should commit first op", ), - ReplicationTestCase( - "atomic_true_dup_key", - docs=[{"_id": 1, "x": 1}], - command=lambda ctx: { - "applyOps": [ - {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, - {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, - ], - "allowAtomic": True, - }, - expected={"ok": Eq(1.0), "applied": Eq(2)}, - msg="applyOps with allowAtomic: true should succeed on duplicate key", - ), ] APPLYOPS_MULTI_OPS_AND_OPTIONS_TESTS: list[ReplicationTestCase] = ( From c9d40e39de015e80ddc6b442c9aabfbec64a7b07 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 12:58:37 -0700 Subject: [PATCH 11/15] use accurate test names Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_applyOps_entry_validation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index 5d43bb88e..fb8bb1265 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -177,7 +177,7 @@ # _id and must match an existing document. APPLYOPS_O2_FIELD_TESTS: list[ReplicationTestCase] = [ ReplicationTestCase( - "update_o2_no_match", + "update_o2_unmatched_id", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { "applyOps": [ @@ -190,10 +190,10 @@ ], }, error_code=UNKNOWN_ERROR, - msg="applyOps should error when o2 does not match any document", + msg="applyOps should error when o2._id does not match any document", ), ReplicationTestCase( - "update_o2_empty", + "update_o2_missing_id", docs=[{"_id": 1, "x": 1}], command=lambda ctx: { "applyOps": [ @@ -206,7 +206,7 @@ ], }, error_code=NO_SUCH_KEY_ERROR, - msg="applyOps should reject update with empty o2 (missing _id)", + msg="applyOps should reject update when o2 is missing _id", ), ] From 6a5bed9ae9d6db8d2f2a290493f5d5f1a78658fe Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 13:00:51 -0700 Subject: [PATCH 12/15] convert idempotent tests to use ReplicationTestCase Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_applyOps_core.py | 105 ++++++++++-------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index ceb20e384..0f47e498c 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -344,62 +344,69 @@ def test_applyOps_core(database_client, collection, test): # Property [Idempotent Operations]: applying the same operation twice succeeds. - - -def test_applyOps_idempotent_delete(collection): - """Test applying the same delete twice succeeds silently.""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 1, "x": 1}) - execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, - ) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "d", "ns": ns, "o": {"_id": 1}}]}, - ) - assertResult( - result, +APPLYOPS_IDEMPOTENT_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "idempotent_delete", + docs=[{"_id": 1, "x": 1}], + setup=lambda coll: execute_admin_command( + coll, + {"applyOps": [{"op": "d", "ns": f"{coll.database.name}.{coll.name}", "o": {"_id": 1}}]}, + ), + command=lambda ctx: { + "applyOps": [{"op": "d", "ns": ctx.namespace, "o": {"_id": 1}}], + }, expected={"ok": Eq(1.0)}, msg="applyOps should succeed silently when deleting an already-deleted document", - raw_res=True, - ) - - -def test_applyOps_idempotent_insert(collection): - """Test applying the same insert twice succeeds (duplicate key is silently handled).""" - ns = f"{collection.database.name}.{collection.name}" - collection.insert_one({"_id": 0, "setup": True}) - execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 1}}]}, - ) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "i", "ns": ns, "o": {"_id": 1, "x": 2}}]}, - ) - assertResult( - result, + ), + ReplicationTestCase( + "idempotent_insert", + docs=[{"_id": 0, "setup": True}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 1}, + } + ] + }, + ), + command=lambda ctx: { + "applyOps": [{"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 2}}], + }, expected={"ok": Eq(1.0), "applied": Eq(1), "results": Eq([True])}, msg="applyOps should succeed on duplicate _id insert (second apply)", - raw_res=True, - ) + ), + ReplicationTestCase( + "idempotent_noop", + docs=[], + setup=lambda coll: execute_admin_command( + coll, + {"applyOps": [{"op": "n", "ns": f"{coll.database.name}.{coll.name}", "o": {}}]}, + ), + command=lambda ctx: { + "applyOps": [{"op": "n", "ns": ctx.namespace, "o": {}}], + }, + expected={"ok": Eq(1.0)}, + msg="applyOps should succeed when applying no-op twice", + ), +] -def test_applyOps_idempotent_noop(collection): - """Test applying no-op twice succeeds.""" - ns = f"{collection.database.name}.{collection.name}" - execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) - result = execute_admin_command( - collection, - {"applyOps": [{"op": "n", "ns": ns, "o": {}}]}, - ) +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_IDEMPOTENT_TESTS)) +def test_applyOps_idempotent(database_client, collection, test): + """Test applyOps idempotent operations.""" + collection = test.prepare(database_client, collection) + if test.setup: + test.setup(collection) + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) assertResult( result, - expected={"ok": Eq(1.0)}, - msg="applyOps should succeed when applying no-op twice", + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, raw_res=True, ) From 79b278106b102967ee80b2aab553fa77bcc606e3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 14:17:08 -0700 Subject: [PATCH 13/15] update error message Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_applyOps_entry_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py index fb8bb1265..2b42bdd0c 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_entry_validation.py @@ -190,7 +190,7 @@ ], }, error_code=UNKNOWN_ERROR, - msg="applyOps should error when o2._id does not match any document", + msg="applyOps should reject update when o2._id does not match any document", ), ReplicationTestCase( "update_o2_missing_id", From ee51d9c6c5ea2c0aca435c9a6c789aeee56248ff Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 18 Jun 2026 14:57:27 -0700 Subject: [PATCH 14/15] add checks for results Signed-off-by: Alina (Xi) Li --- .../commands/applyOps/test_applyOps_core.py | 82 ++++++++++++ .../applyOps/test_applyOps_multi_ops.py | 126 +++++++++++++++++- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py index 0f47e498c..38b74be2f 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_core.py @@ -410,3 +410,85 @@ def test_applyOps_idempotent(database_client, collection, test): msg=test.msg, raw_res=True, ) + + +# Property [Document Effect Verification]: applyOps operations produce the +# expected documents in the collection. +APPLYOPS_DOC_CHECK_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "doc_check_insert", + docs=[{"_id": 0, "setup": True}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 1}, + } + ] + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}}, + use_admin=False, + expected=[{"_id": 1, "x": 1}], + msg="applyOps insert should create the document", + ), + ReplicationTestCase( + "doc_check_update", + docs=[{"_id": 1, "x": 1}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "u", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 2, "y": 3}, + "o2": {"_id": 1}, + } + ] + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}}, + use_admin=False, + expected=[{"_id": 1, "x": 2, "y": 3}], + msg="applyOps update should modify the document", + ), + ReplicationTestCase( + "doc_check_delete", + docs=[{"_id": 1, "x": 1}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "d", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1}, + } + ] + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}}, + use_admin=False, + expected=[], + msg="applyOps delete should remove the document", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_DOC_CHECK_TESTS)) +def test_applyOps_doc_check(database_client, collection, test): + """Test applyOps operations produce the expected documents.""" + collection = test.prepare(database_client, collection) + if test.setup: + test.setup(collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index 8a20f2ef6..d82db8480 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -14,7 +14,7 @@ from documentdb_tests.compatibility.tests.system.replication.utils.replication_test_case import ( # noqa: E501 ReplicationTestCase, ) -from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.assertions import assertResult, assertSuccess from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -198,3 +198,127 @@ def test_applyOps_multi_ops(database_client, collection, test): msg=test.msg, raw_res=True, ) + + +# Property [Multi-Op Document Effect Verification]: multi-operation batches +# and allowAtomic produce the expected documents in the collection. +APPLYOPS_MULTI_OPS_DOC_CHECK_TESTS: list[ReplicationTestCase] = [ + ReplicationTestCase( + "doc_check_insert_then_update", + docs=[{"_id": 0, "setup": True}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 1}, + }, + { + "op": "u", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 2}, + "o2": {"_id": 1}, + }, + ] + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}}, + use_admin=False, + expected=[{"_id": 1, "x": 2}], + msg="applyOps should leave the updated document after insert-then-update", + ), + ReplicationTestCase( + "doc_check_insert_then_delete", + docs=[{"_id": 0, "setup": True}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 1}, + }, + { + "op": "d", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1}, + }, + ] + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}}, + use_admin=False, + expected=[], + msg="applyOps should leave no document after insert-then-delete", + ), + ReplicationTestCase( + "doc_check_atomic_false_partial_commit", + docs=[{"_id": 1, "x": 1}], + setup=lambda coll: execute_admin_command( + coll, + { + "applyOps": [ + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 2, "x": 2}, + }, + { + "op": "i", + "ns": f"{coll.database.name}.{coll.name}", + "o": {"_id": 1, "x": 3}, + }, + ], + "allowAtomic": False, + }, + ), + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 2}}, + use_admin=False, + expected=[{"_id": 2, "x": 2}], + msg="applyOps with allowAtomic: false should have committed the first insert", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(APPLYOPS_MULTI_OPS_DOC_CHECK_TESTS)) +def test_applyOps_multi_ops_doc_check(database_client, collection, test): + """Test applyOps multi-operation document effects.""" + collection = test.prepare(database_client, collection) + if test.setup: + test.setup(collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + msg=test.msg, + ) + + +def test_applyOps_cross_namespace_doc_check(database_client, collection): + """Test applyOps cross-namespace inserts create documents in both collections.""" + db = database_client + coll_name = collection.name + ns1 = f"{db.name}.{coll_name}_ns1" + ns2 = f"{db.name}.{coll_name}_ns2" + db.create_collection(f"{coll_name}_ns1") + db.create_collection(f"{coll_name}_ns2") + execute_admin_command( + collection, + { + "applyOps": [ + {"op": "i", "ns": ns1, "o": {"_id": 1, "src": "coll1"}}, + {"op": "i", "ns": ns2, "o": {"_id": 1, "src": "coll2"}}, + ] + }, + ) + coll1 = db[f"{coll_name}_ns1"] + result1 = execute_command(coll1, {"find": coll1.name, "filter": {"_id": 1}}) + assertSuccess( + result1, + [{"_id": 1, "src": "coll1"}], + msg="applyOps should create document in first namespace", + ) From d7451b36c7e617c5dd684e5454896cc1149afb25 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 19 Jun 2026 14:47:55 -0700 Subject: [PATCH 15/15] use update to cause error Signed-off-by: Alina (Xi) Li --- .../applyOps/test_applyOps_multi_ops.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py index d82db8480..ec0932686 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/applyOps/test_applyOps_multi_ops.py @@ -15,6 +15,7 @@ ReplicationTestCase, ) from documentdb_tests.framework.assertions import assertResult, assertSuccess +from documentdb_tests.framework.error_codes import UNKNOWN_ERROR from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -168,12 +169,17 @@ command=lambda ctx: { "applyOps": [ {"op": "i", "ns": ctx.namespace, "o": {"_id": 2, "x": 2}}, - {"op": "i", "ns": ctx.namespace, "o": {"_id": 1, "x": 3}}, + { + "op": "u", + "ns": ctx.namespace, + "o": {"_id": 999, "x": 3}, + "o2": {"_id": 999}, + }, ], "allowAtomic": False, }, - expected={"ok": Eq(1.0)}, - msg="applyOps with allowAtomic: false should commit first op", + error_code=UNKNOWN_ERROR, + msg="applyOps with allowAtomic: false reports error when second op fails", ), ] @@ -267,9 +273,10 @@ def test_applyOps_multi_ops(database_client, collection, test): "o": {"_id": 2, "x": 2}, }, { - "op": "i", + "op": "u", "ns": f"{coll.database.name}.{coll.name}", - "o": {"_id": 1, "x": 3}, + "o": {"_id": 999, "x": 3}, + "o2": {"_id": 999}, }, ], "allowAtomic": False,