From 25ae4e0e8c8630309de9d7054ab8f3710495405c Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 6 Jul 2026 15:16:46 -0700 Subject: [PATCH] commit Transaction tests Signed-off-by: Alina (Xi) Li --- .../commands/commitTransaction/__init__.py | 0 .../test_commitTransaction.py | 67 +++++ .../test_commitTransaction_atomicity.py | 28 ++ ...test_commitTransaction_autocommit_error.py | 116 ++++++++ .../test_commitTransaction_comment.py | 144 ++++++++++ .../test_commitTransaction_core_error.py | 110 ++++++++ ...commitTransaction_disallowed_operations.py | 245 +++++++++++++++++ .../test_commitTransaction_field_types.py | 176 ++++++++++++ .../test_commitTransaction_op_writeconcern.py | 29 ++ ..._commitTransaction_supported_operations.py | 217 +++++++++++++++ ...test_commitTransaction_txn_number_error.py | 131 +++++++++ ...itTransaction_unrecognized_fields_error.py | 106 ++++++++ .../test_commitTransaction_visibility.py | 40 +++ .../test_commitTransaction_write_conflict.py | 34 +++ .../test_commitTransaction_writeconcern.py | 192 +++++++++++++ ...st_commitTransaction_writeconcern_error.py | 256 ++++++++++++++++++ .../test_smoke_commitTransaction.py | 2 +- documentdb_tests/framework/error_codes.py | 4 + 18 files changed, 1896 insertions(+), 1 deletion(-) create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_atomicity.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_autocommit_error.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_comment.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_core_error.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_disallowed_operations.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_field_types.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_op_writeconcern.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_supported_operations.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_txn_number_error.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_unrecognized_fields_error.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_visibility.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_write_conflict.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern.py create mode 100644 documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern_error.py diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/__init__.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction.py new file mode 100644 index 000000000..34af5a44d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction.py @@ -0,0 +1,67 @@ +"""Tests for commitTransaction command success behavior. + +Each test drives the full transaction lifecycle inline (start session, start +transaction, run an operation, commit) so that setup -> act -> assert reads +top to bottom. These cover the fundamental commit behaviors: a committed write +is durable, an empty transaction commits, the response carries ok:1, and a +commit is retryable. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import ( + assertNotError, + assertSuccess, + assertSuccessPartial, +) +from documentdb_tests.framework.executor import execute_admin_command, execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_commitTransaction_persists_insert(collection): + """A committed insert is visible outside the transaction after commit.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1, "x": "inserted"}, session=session) + session.commit_transaction() + readback = execute_command(collection, {"find": collection.name, "filter": {}}) + assertSuccess(readback, [{"_id": 1, "x": "inserted"}]) + + +def test_commitTransaction_empty(collection): + """Committing a transaction with no operations succeeds.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + session.commit_transaction() + readback = execute_command(collection, {"find": collection.name, "filter": {}}) + assertNotError(readback, msg="commitTransaction on an empty transaction should not error") + + +def test_commitTransaction_response_ok(collection): + """The commitTransaction command response reports ok:1 on success.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + result = execute_admin_command(collection, {"commitTransaction": 1}, session=session) + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_commitTransaction_is_retryable(collection): + """Re-sending commitTransaction after a successful commit returns ok:1. + + The committed state is retained for retryability, so the retry is a no-op + that succeeds rather than an error. + """ + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + execute_admin_command(collection, {"commitTransaction": 1}, session=session) + retry = execute_admin_command(collection, {"commitTransaction": 1}, session=session) + assertSuccessPartial(retry, {"ok": 1.0}) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_atomicity.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_atomicity.py new file mode 100644 index 000000000..628e98aee --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_atomicity.py @@ -0,0 +1,28 @@ +"""Tests for cross-collection atomicity of a committed transaction. + +Writes to more than one collection in a single transaction are committed +atomically: after commit, the writes to the second collection are durable, +demonstrating the commit spans every collection touched in the transaction. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_commit_persists_writes_across_collections(collection): + """Committing persists writes made to a second collection in the same txn.""" + client = collection.database.client + other = collection.database[f"{collection.name}_other"] + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + other.insert_one({"_id": 2}, session=session) + session.commit_transaction() + readback = execute_command(collection, {"find": other.name, "filter": {}}) + assertSuccess(readback, [{"_id": 2}]) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_autocommit_error.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_autocommit_error.py new file mode 100644 index 000000000..47246e503 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_autocommit_error.py @@ -0,0 +1,116 @@ +"""Tests for commitTransaction autocommit parameter validation. + +Validates type and value acceptance for the autocommit parameter. Per the +MongoDB documentation, autocommit must be literal boolean false. Boolean true +produces InvalidOptions, non-boolean types produce TypeMismatch, and null is +treated as omitted (falls through to the no-transaction error). +""" + +from __future__ import annotations + +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 +from documentdb_tests.framework.error_codes import ( + COMMAND_FAILED_ERROR, + INVALID_OPTIONS_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# Property [autocommit Boolean Values]: autocommit accepts only boolean values. +AUTOCOMMIT_BOOLEAN_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "autocommit_bool_false", + command={"commitTransaction": 1, "autocommit": False}, + error_code=INVALID_OPTIONS_ERROR, + msg="commitTransaction should reject autocommit:false outside txn with InvalidOptions", + ), + CommandTestCase( + "autocommit_bool_true", + command={"commitTransaction": 1, "autocommit": True}, + error_code=INVALID_OPTIONS_ERROR, + msg="commitTransaction should reject autocommit:true with InvalidOptions", + ), +] + +# Property [autocommit Type Strictness]: non-boolean types are rejected with TypeMismatch. +AUTOCOMMIT_TYPE_REJECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "autocommit_int32_zero", + command={"commitTransaction": 1, "autocommit": 0}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:0 (int32) as wrong type", + ), + CommandTestCase( + "autocommit_int32_one", + command={"commitTransaction": 1, "autocommit": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:1 (int32) as wrong type", + ), + CommandTestCase( + "autocommit_int64_zero", + command={"commitTransaction": 1, "autocommit": Int64(0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:Int64(0) as wrong type", + ), + CommandTestCase( + "autocommit_double_zero", + command={"commitTransaction": 1, "autocommit": 0.0}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:0.0 as wrong type", + ), + CommandTestCase( + "autocommit_decimal128_zero", + command={"commitTransaction": 1, "autocommit": Decimal128("0")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:Decimal128('0') as wrong type", + ), + CommandTestCase( + "autocommit_string", + command={"commitTransaction": 1, "autocommit": "false"}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:'false' (string) as wrong type", + ), + CommandTestCase( + "autocommit_object", + command={"commitTransaction": 1, "autocommit": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:{} (object) as wrong type", + ), + CommandTestCase( + "autocommit_array", + command={"commitTransaction": 1, "autocommit": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject autocommit:[] (array) as wrong type", + ), +] + +# Property [autocommit Null Handling]: null autocommit is treated as omitted. +AUTOCOMMIT_NULL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "autocommit_null", + command={"commitTransaction": 1, "autocommit": None}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction should treat autocommit:null as omitted", + ), +] + +AUTOCOMMIT_TESTS: list[CommandTestCase] = ( + AUTOCOMMIT_BOOLEAN_TESTS + AUTOCOMMIT_TYPE_REJECTION_TESTS + AUTOCOMMIT_NULL_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(AUTOCOMMIT_TESTS)) +def test_commitTransaction_autocommit_error(collection, test): + """Test commitTransaction autocommit parameter validation.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_comment.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_comment.py new file mode 100644 index 000000000..87f49d7ed --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_comment.py @@ -0,0 +1,144 @@ +"""Tests for commitTransaction comment parameter type acceptance in a real transaction. + +Validates that the comment parameter accepts any BSON type when +commitTransaction is issued inside an active transaction on a replica set. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + +# Property [comment Type Acceptance]: comment accepts any BSON type. +COMMENT_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "comment_string", + command={"commitTransaction": 1, "comment": "test comment"}, + msg="commitTransaction should accept comment:string", + ), + CommandTestCase( + "comment_string_empty", + command={"commitTransaction": 1, "comment": ""}, + msg="commitTransaction should accept comment:empty string", + ), + CommandTestCase( + "comment_int32", + command={"commitTransaction": 1, "comment": 42}, + msg="commitTransaction should accept comment:int32", + ), + CommandTestCase( + "comment_int64", + command={"commitTransaction": 1, "comment": Int64(42)}, + msg="commitTransaction should accept comment:Int64", + ), + CommandTestCase( + "comment_double", + command={"commitTransaction": 1, "comment": 3.14}, + msg="commitTransaction should accept comment:double", + ), + CommandTestCase( + "comment_decimal128", + command={"commitTransaction": 1, "comment": Decimal128("1.5")}, + msg="commitTransaction should accept comment:Decimal128", + ), + CommandTestCase( + "comment_bool_true", + command={"commitTransaction": 1, "comment": True}, + msg="commitTransaction should accept comment:true", + ), + CommandTestCase( + "comment_bool_false", + command={"commitTransaction": 1, "comment": False}, + msg="commitTransaction should accept comment:false", + ), + CommandTestCase( + "comment_null", + command={"commitTransaction": 1, "comment": None}, + msg="commitTransaction should accept comment:null", + ), + CommandTestCase( + "comment_object", + command={"commitTransaction": 1, "comment": {"key": "value"}}, + msg="commitTransaction should accept comment:object", + ), + CommandTestCase( + "comment_object_empty", + command={"commitTransaction": 1, "comment": {}}, + msg="commitTransaction should accept comment:empty object", + ), + CommandTestCase( + "comment_array", + command={"commitTransaction": 1, "comment": [1, 2, 3]}, + msg="commitTransaction should accept comment:array", + ), + CommandTestCase( + "comment_array_empty", + command={"commitTransaction": 1, "comment": []}, + msg="commitTransaction should accept comment:empty array", + ), + CommandTestCase( + "comment_objectid", + command={"commitTransaction": 1, "comment": ObjectId()}, + msg="commitTransaction should accept comment:ObjectId", + ), + CommandTestCase( + "comment_datetime", + command={ + "commitTransaction": 1, + "comment": datetime(2024, 1, 1, tzinfo=timezone.utc), + }, + msg="commitTransaction should accept comment:datetime", + ), + CommandTestCase( + "comment_binary", + command={"commitTransaction": 1, "comment": Binary(b"\x00")}, + msg="commitTransaction should accept comment:Binary", + ), + CommandTestCase( + "comment_regex", + command={"commitTransaction": 1, "comment": Regex(".*")}, + msg="commitTransaction should accept comment:Regex", + ), + CommandTestCase( + "comment_timestamp", + command={"commitTransaction": 1, "comment": Timestamp(0, 0)}, + msg="commitTransaction should accept comment:Timestamp", + ), + CommandTestCase( + "comment_minkey", + command={"commitTransaction": 1, "comment": MinKey()}, + msg="commitTransaction should accept comment:MinKey", + ), + CommandTestCase( + "comment_maxkey", + command={"commitTransaction": 1, "comment": MaxKey()}, + msg="commitTransaction should accept comment:MaxKey", + ), + CommandTestCase( + "comment_code", + command={"commitTransaction": 1, "comment": Code("function(){}")}, + msg="commitTransaction should accept comment:Code", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COMMENT_TYPE_TESTS)) +def test_commitTransaction_comment(collection, test): + """Test commitTransaction comment parameter type acceptance in a transaction.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + result = execute_admin_command(collection, test.command, session=session) + assertSuccessPartial(result, {"ok": 1.0}, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_core_error.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_core_error.py new file mode 100644 index 000000000..f18a720b8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_core_error.py @@ -0,0 +1,110 @@ +"""Tests for commitTransaction command core behavior. + +Validates fundamental command behavior including the no-transaction error, +admin database requirement, and parameter interactions. +""" + +from __future__ import annotations + +import pytest +from bson import Binary, Int64 + +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 ( + COMMAND_FAILED_ERROR, + INVALID_OPTIONS_ERROR, + NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + TRANSACTION_TOO_OLD_ERROR, + UNAUTHORIZED_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# Property [No-Transaction Error]: commitTransaction outside a transaction fails. +CORE_NO_TRANSACTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "no_transaction_basic", + command={"commitTransaction": 1}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction outside a transaction should fail", + ), +] + +# Property [Parameter Acceptance]: all valid parameters combined are syntactically accepted. +CORE_PARAMETER_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "all_valid_params_no_parse_error", + command={ + "commitTransaction": 1, + "autocommit": False, + "txnNumber": Int64(1), + "writeConcern": {"w": "majority", "j": True, "wtimeout": 10_000}, + "comment": "full commit", + }, + error_code=TRANSACTION_TOO_OLD_ERROR, + msg="commitTransaction with all valid params should not produce a parsing error", + ), +] + +# Property [Parameter Interactions]: combinations of valid parameters behave correctly. +CORE_PARAMETER_INTERACTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "interaction_autocommit_only", + command={"commitTransaction": 1, "autocommit": False}, + error_code=INVALID_OPTIONS_ERROR, + msg="commitTransaction with autocommit:false only should fail with InvalidOptions", + ), + CommandTestCase( + "interaction_txn_number_only", + command={"commitTransaction": 1, "txnNumber": Int64(1)}, + error_code=NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + msg="commitTransaction with txnNumber only should fail with NotARetryableWriteCommand", + ), + CommandTestCase( + "interaction_autocommit_txn_number", + command={"commitTransaction": 1, "autocommit": False, "txnNumber": Int64(1)}, + error_code=TRANSACTION_TOO_OLD_ERROR, + msg="commitTransaction with autocommit + txnNumber should fail with TransactionTooOld", + ), + CommandTestCase( + "interaction_lsid", + command={"commitTransaction": 1, "lsid": {"id": Binary(b"\x00" * 16, 4)}}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction with explicit lsid should accept the field", + ), +] + +CORE_TESTS: list[CommandTestCase] = ( + CORE_NO_TRANSACTION_TESTS + CORE_PARAMETER_ACCEPTANCE_TESTS + CORE_PARAMETER_INTERACTION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(CORE_TESTS)) +def test_commitTransaction_core_error(collection, test): + """Test commitTransaction core behavior.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# Property [Admin Database Requirement]: commitTransaction must run against the admin database. +ADMIN_DB_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "non_admin_database", + command={"commitTransaction": 1}, + error_code=UNAUTHORIZED_ERROR, + msg="commitTransaction on a non-admin database should fail with Unauthorized", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ADMIN_DB_TESTS)) +def test_commitTransaction_admin_db_required_error(collection, test): + """Test commitTransaction requires admin database.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_disallowed_operations.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_disallowed_operations.py new file mode 100644 index 000000000..2492910d3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_disallowed_operations.py @@ -0,0 +1,245 @@ +"""Tests for operations that are not supported inside a transaction. + +Which operation types may run inside a transaction is its own concern (kept out +of the behavioral files). A range of commands and aggregation stages are +rejected with OperationNotSupportedInTransaction when issued inside an active +transaction, as are writes to a capped collection or to a system database. Each +test issues the operation in the transaction and asserts the rejection; the +session is closed by its ``with`` block. +""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# Commands rejected inside a transaction with OperationNotSupportedInTransaction. +DISALLOWED_COMMANDS: list[CommandTestCase] = [ + CommandTestCase( + "count", + command=lambda ctx: {"count": ctx.collection}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="count command is not supported inside a transaction", + ), + CommandTestCase( + "listCollections", + command={"listCollections": 1}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="listCollections is not supported inside a transaction", + ), + CommandTestCase( + "listIndexes", + command=lambda ctx: {"listIndexes": ctx.collection}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="listIndexes is not supported inside a transaction", + ), + CommandTestCase( + "explain", + command=lambda ctx: {"explain": {"find": ctx.collection}}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="explain is not supported inside a transaction", + ), + CommandTestCase( + "createUser", + command={"createUser": "commit_txn_user", "pwd": "commit_txn_pwd", "roles": []}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="createUser is not supported inside a transaction", + ), + CommandTestCase( + "getParameter", + command={"getParameter": 1, "logLevel": 1}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="getParameter is not supported inside a transaction", + ), + CommandTestCase( + "setParameter", + command={"setParameter": 1, "logLevel": 0}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="setParameter is not supported inside a transaction", + ), + CommandTestCase( + "killCursors", + command=lambda ctx: {"killCursors": ctx.collection, "cursors": [Int64(0)]}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="killCursors is not supported as the first operation in a transaction", + ), + CommandTestCase( + "hello", + command={"hello": 1}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="hello is not supported as the first operation in a transaction", + ), + CommandTestCase( + "buildInfo", + command={"buildInfo": 1}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="buildInfo is not supported as the first operation in a transaction", + ), + CommandTestCase( + "connectionStatus", + command={"connectionStatus": 1}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="connectionStatus is not supported as the first operation in a transaction", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DISALLOWED_COMMANDS)) +def test_command_disallowed_in_transaction(collection, test): + """Commands not supported inside a transaction are rejected.""" + collection.insert_one({"_id": 1}) + ctx = CommandContext.from_collection(collection) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command(collection, test.build_command(ctx), session=session) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# Aggregation stages rejected inside a transaction (run against the fixture +# collection via the aggregate command). +DISALLOWED_AGGREGATION_STAGES: list[CommandTestCase] = [ + CommandTestCase( + "out", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$out": f"{ctx.collection}_out"}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$out is not supported inside a transaction", + ), + CommandTestCase( + "merge", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$merge": {"into": f"{ctx.collection}_merge"}}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$merge is not supported inside a transaction", + ), + CommandTestCase( + "unionWith", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$unionWith": ctx.collection}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$unionWith is not supported inside a transaction", + ), + CommandTestCase( + "collStats", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$collStats": {"count": {}}}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$collStats is not supported inside a transaction", + ), + CommandTestCase( + "indexStats", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$indexStats": {}}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$indexStats is not supported inside a transaction", + ), + CommandTestCase( + "planCacheStats", + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$planCacheStats": {}}], + "cursor": {}, + }, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$planCacheStats is not supported inside a transaction", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DISALLOWED_AGGREGATION_STAGES)) +def test_aggregation_stage_disallowed_in_transaction(collection, test): + """Aggregation stages not supported inside a transaction are rejected.""" + collection.insert_one({"_id": 1}) + ctx = CommandContext.from_collection(collection) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command(collection, test.build_command(ctx), session=session) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# Admin-level (database) aggregation stages rejected inside a transaction. +DISALLOWED_ADMIN_STAGES: list[CommandTestCase] = [ + CommandTestCase( + "currentOp", + command={"aggregate": 1, "pipeline": [{"$currentOp": {}}], "cursor": {}}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$currentOp is not supported inside a transaction", + ), + CommandTestCase( + "listLocalSessions", + command={"aggregate": 1, "pipeline": [{"$listLocalSessions": {}}], "cursor": {}}, + error_code=OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="$listLocalSessions is not supported inside a transaction", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DISALLOWED_ADMIN_STAGES)) +def test_admin_aggregation_stage_disallowed_in_transaction(collection, test): + """Admin-level aggregation stages not supported inside a transaction are rejected.""" + collection.insert_one({"_id": 1}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_admin_command(collection, test.command, session=session) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_write_to_capped_collection_disallowed_in_transaction(collection): + """Writing to a capped collection inside a transaction is not supported.""" + capped = collection.database.create_collection( + f"{collection.name}_capped", capped=True, size=4096 + ) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + {"insert": capped.name, "documents": [{"_id": 1}]}, + session=session, + ) + assertFailureCode(result, OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR) + + +@pytest.mark.parametrize("system_db_name", ["config", "local"]) +def test_write_to_system_database_disallowed_in_transaction(collection, system_db_name): + """Writing to a system database inside a transaction is not supported.""" + client = collection.database.client + system_collection = client[system_db_name][f"{collection.name}_probe"] + with client.start_session() as session: + session.start_transaction() + result = execute_command( + system_collection, + {"insert": system_collection.name, "documents": [{"_id": 1}]}, + session=session, + ) + assertFailureCode(result, OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_field_types.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_field_types.py new file mode 100644 index 000000000..624c5284e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_field_types.py @@ -0,0 +1,176 @@ +"""Tests for commitTransaction command field type acceptance in a real transaction. + +Validates that the commitTransaction command's primary field accepts all BSON +types when issued inside an active transaction on a replica set. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + +# Property [Field Type Acceptance]: the command field accepts any BSON type. +FIELD_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "field_int32_positive", + command={"commitTransaction": 1}, + msg="commitTransaction should accept int32 positive value", + ), + CommandTestCase( + "field_int32_negative", + command={"commitTransaction": -1}, + msg="commitTransaction should accept int32 negative value", + ), + CommandTestCase( + "field_int32_zero", + command={"commitTransaction": 0}, + msg="commitTransaction should accept int32 zero value", + ), + CommandTestCase( + "field_int64", + command={"commitTransaction": Int64(1)}, + msg="commitTransaction should accept int64 value", + ), + CommandTestCase( + "field_int64_max", + command={"commitTransaction": Int64(9_223_372_036_854_775_807)}, + msg="commitTransaction should accept int64 max value", + ), + CommandTestCase( + "field_double", + command={"commitTransaction": 1.0}, + msg="commitTransaction should accept double value", + ), + CommandTestCase( + "field_double_negative", + command={"commitTransaction": -1.0}, + msg="commitTransaction should accept negative double value", + ), + CommandTestCase( + "field_double_zero", + command={"commitTransaction": 0.0}, + msg="commitTransaction should accept double zero value", + ), + CommandTestCase( + "field_decimal128", + command={"commitTransaction": Decimal128("1")}, + msg="commitTransaction should accept Decimal128 value", + ), + CommandTestCase( + "field_bool_true", + command={"commitTransaction": True}, + msg="commitTransaction should accept bool true value", + ), + CommandTestCase( + "field_bool_false", + command={"commitTransaction": False}, + msg="commitTransaction should accept bool false value", + ), + CommandTestCase( + "field_nan", + command={"commitTransaction": float("nan")}, + msg="commitTransaction should accept NaN value", + ), + CommandTestCase( + "field_infinity", + command={"commitTransaction": float("inf")}, + msg="commitTransaction should accept Infinity value", + ), + CommandTestCase( + "field_string", + command={"commitTransaction": "commitTransaction"}, + msg="commitTransaction should accept string value", + ), + CommandTestCase( + "field_string_empty", + command={"commitTransaction": ""}, + msg="commitTransaction should accept empty string value", + ), + CommandTestCase( + "field_null", + command={"commitTransaction": None}, + msg="commitTransaction should accept null value", + ), + CommandTestCase( + "field_object_empty", + command={"commitTransaction": {}}, + msg="commitTransaction should accept empty object value", + ), + CommandTestCase( + "field_object_nonempty", + command={"commitTransaction": {"key": "value"}}, + msg="commitTransaction should accept non-empty object value", + ), + CommandTestCase( + "field_array_empty", + command={"commitTransaction": []}, + msg="commitTransaction should accept empty array value", + ), + CommandTestCase( + "field_array_nonempty", + command={"commitTransaction": [1, 2]}, + msg="commitTransaction should accept non-empty array value", + ), + CommandTestCase( + "field_binary", + command={"commitTransaction": Binary(b"\x00")}, + msg="commitTransaction should accept Binary value", + ), + CommandTestCase( + "field_objectid", + command={"commitTransaction": ObjectId()}, + msg="commitTransaction should accept ObjectId value", + ), + CommandTestCase( + "field_datetime", + command={"commitTransaction": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + msg="commitTransaction should accept datetime value", + ), + CommandTestCase( + "field_regex", + command={"commitTransaction": Regex(".*")}, + msg="commitTransaction should accept Regex value", + ), + CommandTestCase( + "field_timestamp", + command={"commitTransaction": Timestamp(0, 0)}, + msg="commitTransaction should accept Timestamp value", + ), + CommandTestCase( + "field_code", + command={"commitTransaction": Code("function(){}")}, + msg="commitTransaction should accept Code value", + ), + CommandTestCase( + "field_minkey", + command={"commitTransaction": MinKey()}, + msg="commitTransaction should accept MinKey value", + ), + CommandTestCase( + "field_maxkey", + command={"commitTransaction": MaxKey()}, + msg="commitTransaction should accept MaxKey value", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIELD_TYPE_TESTS)) +def test_commitTransaction_field_types(collection, test): + """Test commitTransaction command field type acceptance in a transaction.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + result = execute_admin_command(collection, test.command, session=session) + assertSuccessPartial(result, {"ok": 1.0}, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_op_writeconcern.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_op_writeconcern.py new file mode 100644 index 000000000..75740fff0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_op_writeconcern.py @@ -0,0 +1,29 @@ +"""Test that an explicit writeConcern on an operation inside a transaction is rejected. + +Per the MongoDB docs, setting a write concern on the individual write operations +inside a transaction returns an error; the write concern is set on the +transaction as a whole (on commit) instead. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import INVALID_OPTIONS_ERROR +from documentdb_tests.framework.executor import execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_op_level_write_concern_rejected_in_transaction(collection): + """An explicit writeConcern on a write operation inside a transaction is rejected.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + {"insert": collection.name, "documents": [{"_id": 1}], "writeConcern": {"w": 1}}, + session=session, + ) + assertFailureCode(result, INVALID_OPTIONS_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_supported_operations.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_supported_operations.py new file mode 100644 index 000000000..60e72f74c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_supported_operations.py @@ -0,0 +1,217 @@ +"""Tests for operations that are supported inside a transaction. + +Reads (find), aggregation (including ``$count``, the supported counterpart to +the disallowed ``count`` command), writes (update, delete), and a range of other +CRUD/administration commands and query operators are accepted inside a +transaction. Each test runs the operation in the transaction and commits; +acceptance is asserted via the ``ok: 1`` command response (contrast the +disallowed-operations file, where the same operations would be rejected). +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertSuccess, assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command, execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_find_runs_in_transaction(collection): + """A find issued inside a transaction returns the collection's documents.""" + collection.insert_one({"_id": 1}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, {"find": collection.name, "filter": {}}, session=session + ) + session.commit_transaction() + assertSuccess(result, [{"_id": 1}]) + + +def test_aggregate_count_runs_in_transaction(collection): + """An aggregate with $count runs inside a transaction and returns the count.""" + collection.insert_many([{"_id": 1}, {"_id": 2}]) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + {"aggregate": collection.name, "pipeline": [{"$count": "n"}], "cursor": {}}, + session=session, + ) + session.commit_transaction() + assertSuccess(result, [{"n": 2}]) + + +def test_update_runs_in_transaction(collection): + """An update issued inside a transaction is durable after commit.""" + collection.insert_one({"_id": 1, "x": "before"}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.update_one({"_id": 1}, {"$set": {"x": "after"}}, session=session) + session.commit_transaction() + readback = execute_command(collection, {"find": collection.name, "filter": {}}) + assertSuccess(readback, [{"_id": 1, "x": "after"}]) + + +def test_delete_runs_in_transaction(collection): + """A delete issued inside a transaction is durable after commit.""" + collection.insert_one({"_id": 1}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.delete_one({"_id": 1}, session=session) + session.commit_transaction() + readback = execute_command(collection, {"find": collection.name, "filter": {}}) + assertSuccess(readback, []) + + +def test_distinct_runs_in_transaction(collection): + """The distinct command is accepted inside a transaction.""" + collection.insert_many([{"_id": 1, "x": 1}, {"_id": 2, "x": 2}]) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, {"distinct": collection.name, "key": "x"}, session=session + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_findAndModify_runs_in_transaction(collection): + """The findAndModify command is accepted inside a transaction.""" + collection.insert_one({"_id": 1, "x": "before"}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + { + "findAndModify": collection.name, + "query": {"_id": 1}, + "update": {"$set": {"x": "after"}}, + }, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_bulkWrite_runs_in_transaction(collection): + """The bulkWrite command is accepted inside a transaction.""" + client = collection.database.client + namespace = f"{collection.database.name}.{collection.name}" + with client.start_session() as session: + session.start_transaction() + result = execute_admin_command( + collection, + { + "bulkWrite": 1, + "ops": [{"insert": 0, "document": {"_id": 1}}], + "nsInfo": [{"ns": namespace}], + }, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_create_collection_runs_in_transaction(collection): + """Creating a collection inside a transaction is accepted.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command(collection, {"create": f"{collection.name}_new"}, session=session) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_createIndexes_on_new_collection_runs_in_transaction(collection): + """createIndexes is accepted on a collection created earlier in the same transaction.""" + client = collection.database.client + new_collection = f"{collection.name}_new" + with client.start_session() as session: + session.start_transaction() + execute_command(collection, {"create": new_collection}, session=session) + result = execute_command( + collection, + {"createIndexes": new_collection, "indexes": [{"key": {"x": 1}, "name": "x_1"}]}, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_near_query_runs_in_transaction(collection): + """A $near query runs inside a transaction (with a 2dsphere index).""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_one({"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$near": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}} + }, + }, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_nearSphere_query_runs_in_transaction(collection): + """A $nearSphere query runs inside a transaction (with a 2dsphere index).""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_one({"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$nearSphere": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}} + }, + }, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) + + +def test_geoNear_stage_runs_in_transaction(collection): + """A $geoNear aggregation stage runs inside a transaction (with a 2dsphere index).""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_one({"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}) + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$geoNear": { + "near": {"type": "Point", "coordinates": [0, 0]}, + "distanceField": "d", + "spherical": True, + } + } + ], + "cursor": {}, + }, + session=session, + ) + session.commit_transaction() + assertSuccessPartial(result, {"ok": 1.0}) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_txn_number_error.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_txn_number_error.py new file mode 100644 index 000000000..135bd9470 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_txn_number_error.py @@ -0,0 +1,131 @@ +"""Tests for commitTransaction txnNumber parameter validation. + +Validates type acceptance for the txnNumber parameter. Per the MongoDB +documentation, txnNumber is typed as long (Int64). On a replica set, +Int64 values produce NotARetryableWriteCommand (50768) when there is no +matching transaction, while negative values produce InvalidOptions (72). +Non-Int64 numeric types and non-numeric types produce TypeMismatch. Null +is treated as omitted (falls through to the no-transaction error). +""" + +from __future__ import annotations + +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 +from documentdb_tests.framework.error_codes import ( + COMMAND_FAILED_ERROR, + INVALID_OPTIONS_ERROR, + NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# Property [txnNumber Int64 Acceptance]: Int64 values are accepted for txnNumber. +TXN_NUMBER_INT64_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "txn_number_int64_positive", + command={"commitTransaction": 1, "txnNumber": Int64(1)}, + error_code=NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + msg="commitTransaction should accept txnNumber:Int64(1) and fail", + ), + CommandTestCase( + "txn_number_int64_zero", + command={"commitTransaction": 1, "txnNumber": Int64(0)}, + error_code=NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + msg="commitTransaction should accept txnNumber:Int64(0) and fail", + ), + CommandTestCase( + "txn_number_int64_max", + command={"commitTransaction": 1, "txnNumber": Int64(9_223_372_036_854_775_807)}, + error_code=NOT_A_RETRYABLE_WRITE_COMMAND_ERROR, + msg="commitTransaction should accept txnNumber:Int64 max value", + ), + CommandTestCase( + "txn_number_int64_negative", + command={"commitTransaction": 1, "txnNumber": Int64(-1)}, + error_code=INVALID_OPTIONS_ERROR, + msg="commitTransaction should reject negative txnNumber with InvalidOptions", + ), +] + +# Property [txnNumber Type Strictness]: non-Int64 types are rejected with TypeMismatch. +TXN_NUMBER_TYPE_REJECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "txn_number_int32", + command={"commitTransaction": 1, "txnNumber": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:int32 as wrong type", + ), + CommandTestCase( + "txn_number_double_whole", + command={"commitTransaction": 1, "txnNumber": 1.0}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:double (whole) as wrong type", + ), + CommandTestCase( + "txn_number_double_fractional", + command={"commitTransaction": 1, "txnNumber": 1.5}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:double (fractional) as wrong type", + ), + CommandTestCase( + "txn_number_decimal128", + command={"commitTransaction": 1, "txnNumber": Decimal128("1")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:Decimal128 as wrong type", + ), + CommandTestCase( + "txn_number_string", + command={"commitTransaction": 1, "txnNumber": "1"}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:string as wrong type", + ), + CommandTestCase( + "txn_number_bool", + command={"commitTransaction": 1, "txnNumber": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:bool as wrong type", + ), + CommandTestCase( + "txn_number_object", + command={"commitTransaction": 1, "txnNumber": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:{} (object) as wrong type", + ), + CommandTestCase( + "txn_number_array", + command={"commitTransaction": 1, "txnNumber": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject txnNumber:[] (array) as wrong type", + ), +] + +# Property [txnNumber Null Handling]: null txnNumber is treated as omitted. +TXN_NUMBER_NULL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "txn_number_null", + command={"commitTransaction": 1, "txnNumber": None}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction should treat txnNumber:null as omitted", + ), +] + +TXN_NUMBER_TESTS: list[CommandTestCase] = ( + TXN_NUMBER_INT64_TESTS + TXN_NUMBER_TYPE_REJECTION_TESTS + TXN_NUMBER_NULL_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(TXN_NUMBER_TESTS)) +def test_commitTransaction_txn_number_error(collection, test): + """Test commitTransaction txnNumber parameter validation.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_unrecognized_fields_error.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_unrecognized_fields_error.py new file mode 100644 index 000000000..0c591208a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_unrecognized_fields_error.py @@ -0,0 +1,106 @@ +"""Tests for commitTransaction unrecognized field handling. + +Validates that the commitTransaction command rejects unknown fields. Covers +single unknown fields, multiple unknown fields, case-sensitive field names, +known fields from other commands, and dollar-prefixed fields. +""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +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 UNRECOGNIZED_COMMAND_FIELD_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# Property [Unrecognized Field Rejection]: unknown fields are rejected. +UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unknown_single_field", + command={"commitTransaction": 1, "unknownField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject single unknown field", + ), + CommandTestCase( + "unknown_multiple_fields", + command={"commitTransaction": 1, "foo": 1, "bar": 2}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject multiple unknown fields", + ), +] + +# Property [Case Sensitivity]: field names are case-sensitive and wrong-case variants are rejected. +CASE_SENSITIVITY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "case_WriteConcern", + command={"commitTransaction": 1, "WriteConcern": {"w": 1}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject 'WriteConcern' (capital W) as unrecognized", + ), + CommandTestCase( + "case_Autocommit", + command={"commitTransaction": 1, "Autocommit": False}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject 'Autocommit' (capital A) as unrecognized", + ), + CommandTestCase( + "case_TxnNumber", + command={"commitTransaction": 1, "TxnNumber": Int64(1)}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject 'TxnNumber' (capital T) as unrecognized", + ), + CommandTestCase( + "case_Comment", + command={"commitTransaction": 1, "Comment": "test"}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject 'Comment' (capital C) as unrecognized", + ), +] + +# Property [Foreign Field Rejection]: fields from other commands are rejected. +FOREIGN_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "foreign_query", + command={"commitTransaction": 1, "query": {"x": 1}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject 'query' field from other commands", + ), + CommandTestCase( + "dollar_prefixed", + command={"commitTransaction": 1, "$unknown": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject dollar-prefixed unknown field", + ), +] + +# Property [writeConcern Unknown Sub-Field]: unknown writeConcern sub-fields are rejected. +WRITECONCERN_UNKNOWN_SUBFIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "wc_unknown_subfield", + command={"commitTransaction": 1, "writeConcern": {"w": 1, "unknownOption": True}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="commitTransaction should reject unknown writeConcern sub-field", + ), +] + +UNRECOGNIZED_TESTS: list[CommandTestCase] = ( + UNRECOGNIZED_FIELD_TESTS + + CASE_SENSITIVITY_TESTS + + FOREIGN_FIELD_TESTS + + WRITECONCERN_UNKNOWN_SUBFIELD_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(UNRECOGNIZED_TESTS)) +def test_commitTransaction_unrecognized_fields_error(collection, test): + """Test commitTransaction unrecognized field handling.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_visibility.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_visibility.py new file mode 100644 index 000000000..445736b7d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_visibility.py @@ -0,0 +1,40 @@ +"""Tests for the visibility of a transaction's pending writes before commit. + +A write made inside an open transaction must not be visible to readers outside +the transaction until it commits, but must be visible to reads issued within +the same session. Each test observes the pending write before committing, then +commits to end the transaction. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_pending_write_not_visible_outside_before_commit(collection): + """A pending insert is not visible to a reader outside the transaction.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1, "x": 1}, session=session) + outside = execute_command(collection, {"find": collection.name, "filter": {}}) + session.commit_transaction() + assertSuccess(outside, []) + + +def test_pending_write_visible_in_session_before_commit(collection): + """A pending insert is visible to a read in the same session before commit.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1, "x": 1}, session=session) + in_session = execute_command( + collection, {"find": collection.name, "filter": {}}, session=session + ) + session.commit_transaction() + assertSuccess(in_session, [{"_id": 1, "x": 1}]) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_write_conflict.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_write_conflict.py new file mode 100644 index 000000000..27208186c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_write_conflict.py @@ -0,0 +1,34 @@ +"""Test write-conflict behavior for concurrent transactions. + +When two open transactions write to the same document, the second writer fails +immediately with WriteConflict rather than blocking (first-committer-wins). The +first transaction commits; the conflicting session is left for its ``with`` +block to close. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import WRITE_CONFLICT_ERROR +from documentdb_tests.framework.executor import execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +def test_second_writer_to_open_doc_fails_with_write_conflict(collection): + """A second writer to a doc already modified in an open txn fails with WriteConflict.""" + collection.insert_one({"_id": 1, "x": 0}) + client = collection.database.client + with client.start_session() as first, client.start_session() as second: + first.start_transaction() + collection.update_one({"_id": 1}, {"$set": {"x": 1}}, session=first) + second.start_transaction() + result = execute_command( + collection, + {"update": collection.name, "updates": [{"q": {"_id": 1}, "u": {"$set": {"x": 2}}}]}, + session=second, + ) + first.commit_transaction() + assertFailureCode(result, WRITE_CONFLICT_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern.py new file mode 100644 index 000000000..12a254216 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern.py @@ -0,0 +1,192 @@ +"""Tests for commitTransaction writeConcern parameter acceptance in a real transaction. + +Validates that accepted writeConcern variants (document types, w sub-field +values, j sub-field values, wtimeout sub-field values, and edge cases) succeed +when commitTransaction is issued inside an active transaction on a replica set. +""" + +from __future__ import annotations + +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 assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + +# Property [writeConcern Document Acceptance]: writeConcern accepts document values. +WRITECONCERN_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "writeconcern_empty_doc", + command={"commitTransaction": 1, "writeConcern": {}}, + msg="commitTransaction should accept empty writeConcern document", + ), + CommandTestCase( + "writeconcern_null", + command={"commitTransaction": 1, "writeConcern": None}, + msg="commitTransaction should accept writeConcern:null", + ), + CommandTestCase( + "wc_combined_w_j_wtimeout", + command={ + "commitTransaction": 1, + "writeConcern": {"w": "majority", "j": True, "wtimeout": 10_000}, + }, + msg="commitTransaction should accept combined w + j + wtimeout", + ), + CommandTestCase( + "wc_w0_j_true", + command={"commitTransaction": 1, "writeConcern": {"w": 0, "j": True}}, + msg="commitTransaction should accept conflicting w:0 with j:true", + ), + CommandTestCase( + "wc_fsync_true", + command={"commitTransaction": 1, "writeConcern": {"fsync": True}}, + msg="commitTransaction should accept legacy writeConcern.fsync:true", + ), +] + +# Property [w Accepted Values]: w accepts int and string "majority" values. +W_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "w_int32_one", + command={"commitTransaction": 1, "writeConcern": {"w": 1}}, + msg="commitTransaction should accept writeConcern.w:1", + ), + CommandTestCase( + "w_int32_zero", + command={"commitTransaction": 1, "writeConcern": {"w": 0}}, + msg="commitTransaction should accept writeConcern.w:0 (unacknowledged)", + ), + CommandTestCase( + "w_majority", + command={"commitTransaction": 1, "writeConcern": {"w": "majority"}}, + msg="commitTransaction should accept writeConcern.w:'majority'", + ), + CommandTestCase( + "w_int64", + command={"commitTransaction": 1, "writeConcern": {"w": Int64(1)}}, + msg="commitTransaction should accept writeConcern.w:Int64(1)", + ), + CommandTestCase( + "w_double_whole", + command={"commitTransaction": 1, "writeConcern": {"w": 1.0}}, + msg="commitTransaction should accept writeConcern.w:1.0", + ), + CommandTestCase( + "w_double_fractional", + command={"commitTransaction": 1, "writeConcern": {"w": 1.5}}, + msg="commitTransaction should accept writeConcern.w:1.5", + ), + CommandTestCase( + "w_decimal128", + command={"commitTransaction": 1, "writeConcern": {"w": Decimal128("1")}}, + msg="commitTransaction should accept writeConcern.w:Decimal128('1')", + ), +] + +# Property [j Accepted Values]: j accepts boolean and numeric types. +J_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "j_bool_true", + command={"commitTransaction": 1, "writeConcern": {"j": True}}, + msg="commitTransaction should accept writeConcern.j:true", + ), + CommandTestCase( + "j_bool_false", + command={"commitTransaction": 1, "writeConcern": {"j": False}}, + msg="commitTransaction should accept writeConcern.j:false", + ), + CommandTestCase( + "j_int32_one", + command={"commitTransaction": 1, "writeConcern": {"j": 1}}, + msg="commitTransaction should accept writeConcern.j:1 (coerced to true)", + ), + CommandTestCase( + "j_int32_zero", + command={"commitTransaction": 1, "writeConcern": {"j": 0}}, + msg="commitTransaction should accept writeConcern.j:0 (coerced to false)", + ), + CommandTestCase( + "j_null", + command={"commitTransaction": 1, "writeConcern": {"j": None}}, + msg="commitTransaction should accept writeConcern.j:null", + ), +] + +# Property [wtimeout Accepted Values]: wtimeout accepts numeric types broadly. +WTIMEOUT_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "wtimeout_int32_positive", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": 1000}}, + msg="commitTransaction should accept writeConcern.wtimeout:1000", + ), + CommandTestCase( + "wtimeout_int32_zero", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": 0}}, + msg="commitTransaction should accept writeConcern.wtimeout:0 (no timeout)", + ), + CommandTestCase( + "wtimeout_int64", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": Int64(1000)}}, + msg="commitTransaction should accept writeConcern.wtimeout:Int64(1000)", + ), + CommandTestCase( + "wtimeout_double_whole", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": 1000.0}}, + msg="commitTransaction should accept writeConcern.wtimeout:1000.0", + ), + CommandTestCase( + "wtimeout_negative", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": -1}}, + msg="commitTransaction should accept writeConcern.wtimeout:-1", + ), + CommandTestCase( + "wtimeout_string", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": "1000"}}, + msg="commitTransaction should accept writeConcern.wtimeout:'1000'", + ), + CommandTestCase( + "wtimeout_bool", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": True}}, + msg="commitTransaction should accept writeConcern.wtimeout:true", + ), + CommandTestCase( + "wtimeout_null", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": None}}, + msg="commitTransaction should accept writeConcern.wtimeout:null", + ), + CommandTestCase( + "wtimeout_object", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": {}}}, + msg="commitTransaction should accept writeConcern.wtimeout:{}", + ), + CommandTestCase( + "wtimeout_array", + command={"commitTransaction": 1, "writeConcern": {"wtimeout": []}}, + msg="commitTransaction should accept writeConcern.wtimeout:[]", + ), +] + +WRITECONCERN_TESTS: list[CommandTestCase] = ( + WRITECONCERN_ACCEPTANCE_TESTS + + W_ACCEPTANCE_TESTS + + J_ACCEPTANCE_TESTS + + WTIMEOUT_ACCEPTANCE_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(WRITECONCERN_TESTS)) +def test_commitTransaction_writeconcern(collection, test): + """Test commitTransaction writeConcern parameter acceptance in a transaction.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + collection.insert_one({"_id": 1}, session=session) + result = execute_admin_command(collection, test.command, session=session) + assertSuccessPartial(result, {"ok": 1.0}, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern_error.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern_error.py new file mode 100644 index 000000000..832623c01 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_commitTransaction_writeconcern_error.py @@ -0,0 +1,256 @@ +"""Tests for commitTransaction writeConcern parameter error cases. + +Validates that invalid writeConcern types and sub-field values are rejected +with the appropriate error codes. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, 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 ( + COMMAND_FAILED_ERROR, + FAILED_TO_PARSE_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.admin, pytest.mark.requires(transactions=True)] + + +# --------------------------------------------------------------------------- +# Property [writeConcern Type Rejection]: non-document types are rejected with TypeMismatch. +# --------------------------------------------------------------------------- + +WRITECONCERN_TYPE_REJECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "writeconcern_string", + command={"commitTransaction": 1, "writeConcern": "majority"}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:string as wrong type", + ), + CommandTestCase( + "writeconcern_int32", + command={"commitTransaction": 1, "writeConcern": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:int32 as wrong type", + ), + CommandTestCase( + "writeconcern_int64", + command={"commitTransaction": 1, "writeConcern": Int64(1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Int64 as wrong type", + ), + CommandTestCase( + "writeconcern_double", + command={"commitTransaction": 1, "writeConcern": 1.0}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:double as wrong type", + ), + CommandTestCase( + "writeconcern_decimal128", + command={"commitTransaction": 1, "writeConcern": Decimal128("1")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Decimal128 as wrong type", + ), + CommandTestCase( + "writeconcern_bool_true", + command={"commitTransaction": 1, "writeConcern": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:true as wrong type", + ), + CommandTestCase( + "writeconcern_bool_false", + command={"commitTransaction": 1, "writeConcern": False}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:false as wrong type", + ), + CommandTestCase( + "writeconcern_array_empty", + command={"commitTransaction": 1, "writeConcern": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:[] as wrong type", + ), + CommandTestCase( + "writeconcern_array_nonempty", + command={"commitTransaction": 1, "writeConcern": [{"w": 1}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:[{w:1}] as wrong type", + ), + CommandTestCase( + "writeconcern_binary", + command={"commitTransaction": 1, "writeConcern": Binary(b"\x00")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Binary as wrong type", + ), + CommandTestCase( + "writeconcern_objectid", + command={"commitTransaction": 1, "writeConcern": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:ObjectId as wrong type", + ), + CommandTestCase( + "writeconcern_datetime", + command={"commitTransaction": 1, "writeConcern": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:datetime as wrong type", + ), + CommandTestCase( + "writeconcern_regex", + command={"commitTransaction": 1, "writeConcern": Regex(".*")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Regex as wrong type", + ), + CommandTestCase( + "writeconcern_timestamp", + command={"commitTransaction": 1, "writeConcern": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Timestamp as wrong type", + ), + CommandTestCase( + "writeconcern_code", + command={"commitTransaction": 1, "writeConcern": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:Code as wrong type", + ), + CommandTestCase( + "writeconcern_minkey", + command={"commitTransaction": 1, "writeConcern": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:MinKey as wrong type", + ), + CommandTestCase( + "writeconcern_maxkey", + command={"commitTransaction": 1, "writeConcern": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern:MaxKey as wrong type", + ), +] + + +# --------------------------------------------------------------------------- +# Property [w Invalid Values]: invalid w values are rejected with CommandFailed or FailedToParse. +# --------------------------------------------------------------------------- + +W_INVALID_VALUE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "w_custom_tag", + command={"commitTransaction": 1, "writeConcern": {"w": "myTag"}}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction should reject writeConcern.w:'myTag' with CommandFailed", + ), + CommandTestCase( + "w_empty_string", + command={"commitTransaction": 1, "writeConcern": {"w": ""}}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction should reject writeConcern.w:'' with CommandFailed", + ), + CommandTestCase( + "w_null", + command={"commitTransaction": 1, "writeConcern": {"w": None}}, + error_code=COMMAND_FAILED_ERROR, + msg="commitTransaction should reject writeConcern.w:null with CommandFailed", + ), + CommandTestCase( + "w_negative_int", + command={"commitTransaction": 1, "writeConcern": {"w": -1}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:-1 with FailedToParse", + ), + CommandTestCase( + "w_int32_max", + command={"commitTransaction": 1, "writeConcern": {"w": 2_147_483_647}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:INT32_MAX with FailedToParse", + ), + CommandTestCase( + "w_bool_false", + command={"commitTransaction": 1, "writeConcern": {"w": False}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:false with FailedToParse", + ), + CommandTestCase( + "w_bool_true", + command={"commitTransaction": 1, "writeConcern": {"w": True}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:true with FailedToParse", + ), + CommandTestCase( + "w_object", + command={"commitTransaction": 1, "writeConcern": {"w": {}}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:{} with FailedToParse", + ), + CommandTestCase( + "w_array", + command={"commitTransaction": 1, "writeConcern": {"w": []}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.w:[] with FailedToParse", + ), +] + + +# --------------------------------------------------------------------------- +# Property [j Type Rejection]: non-boolean non-numeric types are rejected with TypeMismatch. +# --------------------------------------------------------------------------- + +J_TYPE_REJECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "j_string", + command={"commitTransaction": 1, "writeConcern": {"j": "true"}}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern.j:'true' as wrong type", + ), + CommandTestCase( + "j_object", + command={"commitTransaction": 1, "writeConcern": {"j": {}}}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern.j:{} as wrong type", + ), + CommandTestCase( + "j_array", + command={"commitTransaction": 1, "writeConcern": {"j": []}}, + error_code=TYPE_MISMATCH_ERROR, + msg="commitTransaction should reject writeConcern.j:[] as wrong type", + ), +] + + +# --------------------------------------------------------------------------- +# Property [wtimeout Overflow]: Int64 max value overflows and produces FailedToParse. +# --------------------------------------------------------------------------- + +WTIMEOUT_OVERFLOW_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "wtimeout_int64_max", + command={ + "commitTransaction": 1, + "writeConcern": {"wtimeout": Int64(9_223_372_036_854_775_807)}, + }, + error_code=FAILED_TO_PARSE_ERROR, + msg="commitTransaction should reject writeConcern.wtimeout:Int64 max with FailedToParse", + ), +] + + +WRITECONCERN_ERROR_TESTS: list[CommandTestCase] = ( + WRITECONCERN_TYPE_REJECTION_TESTS + + W_INVALID_VALUE_TESTS + + J_TYPE_REJECTION_TESTS + + WTIMEOUT_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(WRITECONCERN_ERROR_TESTS)) +def test_commitTransaction_writeconcern_error(collection, test): + """Test commitTransaction writeConcern parameter error cases.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_smoke_commitTransaction.py b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_smoke_commitTransaction.py index 3fc6da5e3..9d95e6566 100644 --- a/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_smoke_commitTransaction.py +++ b/documentdb_tests/compatibility/tests/core/sessions/commands/commitTransaction/test_smoke_commitTransaction.py @@ -9,7 +9,7 @@ from documentdb_tests.framework.assertions import assertFailure from documentdb_tests.framework.executor import execute_admin_command -pytestmark = pytest.mark.smoke +pytestmark = [pytest.mark.smoke, pytest.mark.requires(transactions=True)] def test_smoke_commitTransaction(collection): diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 78a86b0c1..85f0ad3d0 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -37,9 +37,11 @@ INDEX_OPTIONS_CONFLICT_ERROR = 85 INDEX_KEY_SPECS_CONFLICT_ERROR = 86 OPERATION_FAILED_ERROR = 96 +WRITE_CONFLICT_ERROR = 112 COMMAND_NOT_SUPPORTED_ERROR = 115 DOCUMENT_VALIDATION_FAILURE_ERROR = 121 NOT_A_REPLICA_SET_ERROR = 123 +COMMAND_FAILED_ERROR = 125 CAPPED_POSITION_LOST_ERROR = 136 INCOMPATIBLE_COLLATION_VERSION_ERROR = 161 VIEW_DEPTH_LIMIT_ERROR = 165 @@ -53,6 +55,7 @@ INVALID_INDEX_SPEC_OPTION_ERROR = 197 INVALID_UUID_ERROR = 207 QUERY_FEATURE_NOT_ALLOWED = 224 +TRANSACTION_TOO_OLD_ERROR = 225 MAX_NESTED_SUB_PIPELINE_ERROR = 232 CONVERSION_FAILURE_ERROR = 241 INVALID_RESUME_TOKEN_ERROR = 260 @@ -368,6 +371,7 @@ TO_TYPE_ARITY_ERROR = 50723 CURSOR_SESSION_MISMATCH_ERROR = 50738 SUBSTR_NEGATIVE_START_ERROR = 50752 +NOT_A_RETRYABLE_WRITE_COMMAND_ERROR = 50768 KEYSTRING_UNKNOWN_TYPE_ERROR = 50811 CLUSTERED_NAN_DUPLICATE_ERROR = 50819 CLUSTERED_INFINITY_DUPLICATE_ERROR = 50826