diff --git a/.gitignore b/.gitignore index 07718a62..27d65ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_basic.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_basic.py new file mode 100644 index 00000000..9598f267 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_basic.py @@ -0,0 +1,95 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.target_collection import ( + NamedCollection, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Basic Drop Response]: drop returns ok:1 with expected fields +# for various collection states. +DROP_BASIC_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "with_documents", + docs=[{"_id": 1, "a": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should return nIndexesWas, ns, and ok", + ), + CommandTestCase( + "empty_collection", + target_collection=NamedCollection(), + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Explicitly created empty collection should have nIndexesWas=1", + ), + CommandTestCase( + "nonexistent", + command={"drop": "nonexistent_coll_xyz"}, + expected={"ok": 1.0}, + msg="Non-existent collection drop should return ok:1", + ), +] + +# Property [Special Name Acceptance]: drop accepts collection names with +# spaces, unicode, and dots. +DROP_SPECIAL_NAME_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "spaces", + target_collection=NamedCollection(suffix=" spaces"), + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with spaces in name", + ), + CommandTestCase( + "unicode", + target_collection=NamedCollection(suffix="_drôp_ünïcödé"), + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with unicode in name", + ), + CommandTestCase( + "dots", + target_collection=NamedCollection(suffix=".dots"), + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with dots in name", + ), +] + +# Property [Null Document Values]: drop succeeds regardless of null values +# in documents. +DROP_NULL_HANDLING_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "null_document_values", + docs=[{"_id": 1, "a": None}, {"_id": 2, "b": None, "c": None}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Drop should succeed regardless of null document values", + ), +] + +DROP_BASIC_ALL_TESTS: list[CommandTestCase] = ( + DROP_BASIC_TESTS + DROP_SPECIAL_NAME_TESTS + DROP_NULL_HANDLING_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_BASIC_ALL_TESTS)) +def test_drop_basic(database_client, collection, test): + """Test basic drop command response.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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/core/collections/commands/drop/test_drop_invalid_names.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_invalid_names.py new file mode 100644 index 00000000..6fcb482a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_invalid_names.py @@ -0,0 +1,100 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import INVALID_NAMESPACE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Invalid Name Type]: drop rejects non-string name values with +# INVALID_NAMESPACE_ERROR. +DROP_INVALID_NAME_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "null", + command={"drop": None}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Null collection name should fail with InvalidNamespace", + ), + CommandTestCase( + "integer", + command={"drop": 123}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Integer collection name should fail with InvalidNamespace", + ), + CommandTestCase( + "boolean", + command={"drop": True}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Boolean collection name should fail with InvalidNamespace", + ), + CommandTestCase( + "double", + command={"drop": 1.5}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Double collection name should fail with InvalidNamespace", + ), + CommandTestCase( + "object", + command={"drop": {"a": 1}}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Object collection name should fail with InvalidNamespace", + ), + CommandTestCase( + "array", + command={"drop": [1, 2]}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Array collection name should fail with InvalidNamespace", + ), +] + +# Property [Invalid Name Value]: drop rejects invalid string name values +# with INVALID_NAMESPACE_ERROR. +DROP_INVALID_NAME_VALUE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "empty_string", + command={"drop": ""}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Empty string name should fail with InvalidNamespace", + ), + CommandTestCase( + "null_byte", + command={"drop": "test\x00coll"}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Name with null byte should fail with InvalidNamespace", + ), + CommandTestCase( + "just_dot", + command={"drop": "."}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Single dot name should fail with InvalidNamespace", + ), + CommandTestCase( + "leading_dot", + command={"drop": ".test"}, + error_code=INVALID_NAMESPACE_ERROR, + msg="Leading dot name should fail with InvalidNamespace", + ), +] + +DROP_INVALID_NAME_TESTS: list[CommandTestCase] = ( + DROP_INVALID_NAME_TYPE_TESTS + DROP_INVALID_NAME_VALUE_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_INVALID_NAME_TESTS)) +def test_drop_invalid_names(database_client, collection, test): + """Test drop command rejects invalid collection names.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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/core/collections/commands/drop/test_drop_options.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_options.py new file mode 100644 index 00000000..a9816cb2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_options.py @@ -0,0 +1,151 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + FAILED_TO_PARSE_ERROR, + TYPE_MISMATCH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [writeConcern Acceptance]: drop accepts valid writeConcern options. +DROP_WRITE_CONCERN_SUCCESS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "w1", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": 1}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with w:1", + ), + CommandTestCase( + "majority", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": "majority"}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with w:majority", + ), + CommandTestCase( + "w0", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": 0}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with w:0", + ), + CommandTestCase( + "wtimeout", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": 1, "wtimeout": 1000}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with wtimeout", + ), + CommandTestCase( + "journal", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": 1, "j": True}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with j:true", + ), + CommandTestCase( + "empty_object", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with empty writeConcern", + ), +] + +# Property [writeConcern Rejection]: drop rejects invalid writeConcern values. +DROP_WRITE_CONCERN_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "non_object", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": "invalid"}, + error_code=TYPE_MISMATCH_ERROR, + msg="Non-object writeConcern should fail with 14", + ), + CommandTestCase( + "negative_w", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "writeConcern": {"w": -1}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="w:-1 should fail with error code 9", + ), +] + +# Property [comment Acceptance]: drop accepts comment of any BSON type. +DROP_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "string", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "comment": "dropping collection"}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with string comment", + ), + CommandTestCase( + "integer", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "comment": 42}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with integer comment", + ), + CommandTestCase( + "object", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "comment": {"reason": "cleanup"}}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with object comment", + ), + CommandTestCase( + "array", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "comment": [1, 2, 3]}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with array comment", + ), + CommandTestCase( + "boolean", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "comment": True}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with boolean comment", + ), +] + +# Property [Unrecognized Field Rejection]: drop rejects unrecognized fields. +DROP_UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unknown_field", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection, "unknownField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Unrecognized field should fail with 40415", + ), +] + +DROP_OPTIONS_TESTS: list[CommandTestCase] = ( + DROP_WRITE_CONCERN_SUCCESS_TESTS + + DROP_WRITE_CONCERN_ERROR_TESTS + + DROP_COMMENT_TESTS + + DROP_UNRECOGNIZED_FIELD_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_OPTIONS_TESTS)) +def test_drop_options(database_client, collection, test): + """Test drop command option acceptance and rejection.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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/core/collections/commands/drop/test_drop_response_fields.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_response_fields.py new file mode 100644 index 00000000..63d47aab --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_response_fields.py @@ -0,0 +1,101 @@ +import pytest +from pymongo import IndexModel + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [nIndexesWas Response]: drop returns nIndexesWas reflecting the +# number of indexes on the collection at drop time. +DROP_NINDEXESWAS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "id_only", + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Should have nIndexesWas=1 for _id only", + ), + CommandTestCase( + "one_additional", + indexes=[IndexModel("a")], + docs=[{"_id": 1, "a": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should have nIndexesWas=2 with one extra index", + ), + CommandTestCase( + "multiple", + indexes=[IndexModel("a"), IndexModel("b"), IndexModel("c")], + docs=[{"_id": 1, "a": 1, "b": 1, "c": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 4, "ns": ctx.namespace, "ok": 1.0}, + msg="Should have nIndexesWas=4 with 3 extra indexes", + ), +] + +# Property [Index Type Acceptance]: drop succeeds on collections with +# various index types and returns correct nIndexesWas. +DROP_INDEX_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "compound", + indexes=[IndexModel([("a", 1), ("b", 1)])], + docs=[{"_id": 1, "a": 1, "b": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should have nIndexesWas=2 with compound index", + ), + CommandTestCase( + "text", + indexes=[IndexModel([("content", "text")])], + docs=[{"_id": 1, "content": "hello world"}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with text index", + ), + CommandTestCase( + "ttl", + indexes=[IndexModel("createdAt", expireAfterSeconds=3600)], + docs=[{"_id": 1, "createdAt": None}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with TTL index", + ), + CommandTestCase( + "unique", + indexes=[IndexModel("email", unique=True)], + docs=[{"_id": 1, "email": "test"}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with unique index", + ), + CommandTestCase( + "hashed", + indexes=[IndexModel([("key", "hashed")])], + docs=[{"_id": 1, "key": "value"}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 2, "ns": ctx.namespace, "ok": 1.0}, + msg="Should succeed with hashed index", + ), +] + +DROP_RESPONSE_TESTS: list[CommandTestCase] = DROP_NINDEXESWAS_TESTS + DROP_INDEX_TYPE_TESTS + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_RESPONSE_TESTS)) +def test_drop_response(database_client, collection, test): + """Test drop command response fields.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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/core/collections/commands/drop/test_drop_special_collections.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_special_collections.py new file mode 100644 index 00000000..34d7af95 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_special_collections.py @@ -0,0 +1,61 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.target_collection import ( + CappedCollection, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Capped Collection Acceptance]: drop succeeds on capped collections. +DROP_CAPPED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "capped", + target_collection=CappedCollection(size=4096), + docs=[{"_id": 1}], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Drop on capped collection should succeed", + ), +] + +# Property [Large Collection Acceptance]: drop succeeds on collections with +# many or large documents. +DROP_LARGE_COLLECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "many_documents", + docs=[{"_id": i, "val": i} for i in range(1000)], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Drop on collection with 1000 docs should succeed", + ), + CommandTestCase( + "large_documents", + docs=[{"_id": i, "data": "x" * 10_000} for i in range(10)], + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"nIndexesWas": 1, "ns": ctx.namespace, "ok": 1.0}, + msg="Drop on collection with large documents should succeed", + ), +] + +DROP_SPECIAL_TESTS: list[CommandTestCase] = DROP_CAPPED_TESTS + DROP_LARGE_COLLECTION_TESTS + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_SPECIAL_TESTS)) +def test_drop_special(database_client, collection, test): + """Test drop on special collection types.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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/core/collections/commands/drop/test_drop_views.py b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_views.py new file mode 100644 index 00000000..7c5e4291 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/drop/test_drop_views.py @@ -0,0 +1,77 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.target_collection import ( + ViewCollection, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [View Drop Acceptance]: drop succeeds on views and returns +# expected response fields. +DROP_VIEW_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "view", + target_collection=ViewCollection(), + command=lambda ctx: {"drop": ctx.collection}, + expected=lambda ctx: {"ns": ctx.namespace, "ok": 1.0}, + msg="Drop on view should return ns and ok without nIndexesWas", + ), +] + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(DROP_VIEW_TESTS)) +def test_drop_views(database_client, collection, test): + """Test drop command behavior on views.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + 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, + ) + + +# Property [Underlying Collection Drop]: drop succeeds on the source +# collection underlying a view. +@pytest.mark.collection_mgmt +def test_drop_underlying_collection(database_client, collection): + """Drop the source collection underlying a view.""" + collection.insert_one({"_id": 1, "a": 1}) + view_name = f"{collection.name}_view" + database_client.command("create", view_name, viewOn=collection.name, pipeline=[]) + result = execute_command(collection, {"drop": collection.name}) + ns = f"{database_client.name}.{collection.name}" + assertResult( + result, + expected={"nIndexesWas": 1, "ns": ns, "ok": 1.0}, + msg="Drop underlying collection should succeed", + raw_res=True, + ) + + +# Property [system.views Drop]: drop succeeds on the system.views collection. +@pytest.mark.collection_mgmt +def test_drop_system_views(database_client, collection): + """Drop the system.views collection.""" + src_name = f"{collection.name}_src" + view_name = f"{collection.name}_view" + database_client.create_collection(src_name) + database_client.command("create", view_name, viewOn=src_name, pipeline=[]) + system_views = database_client["system.views"] + result = execute_command(system_views, {"drop": "system.views"}) + ns = f"{database_client.name}.system.views" + assertResult( + result, + expected={"nIndexesWas": 1, "ns": ns, "ok": 1.0}, + msg="Drop on system.views should succeed", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/utils/__init__.py b/documentdb_tests/compatibility/tests/core/collections/commands/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/collections/commands/utils/command_test_case.py new file mode 100644 index 00000000..cb8fcba5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/utils/command_test_case.py @@ -0,0 +1,87 @@ +"""Shared test case for collection command tests.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + +from pymongo import IndexModel +from pymongo.collection import Collection +from pymongo.database import Database + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.target_collection import ( + TargetCollection, +) +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class CommandContext: + """Runtime fixture values available to command test cases. + + Attributes: + collection: The fixture collection name. + database: The fixture database name. + namespace: The fully qualified namespace (database.collection). + """ + + collection: str + database: str + namespace: str + + @classmethod + def from_collection(cls, collection: Collection) -> CommandContext: + db = collection.database.name + coll = collection.name + return cls(collection=coll, database=db, namespace=f"{db}.{coll}") + + +@dataclass(frozen=True) +class CommandTestCase(BaseTestCase): + """Test case for collection command tests. + + Collection commands often reference fixture-dependent values like + collection names and namespaces. Fields that need these values accept + a callable that receives a CommandContext at execution time. + + Attributes: + target_collection: Describes the collection to execute against. + Defaults to the fixture collection. + indexes: Indexes to create before executing the command. Each + entry is passed to create_index. + docs: Documents to insert before executing the command. + command: A callable (CommandContext -> dict) for commands that + need fixture values, or a plain dict. + expected: A callable (CommandContext -> dict) for results that + need fixture values, a plain dict, or None for error cases. + """ + + target_collection: TargetCollection = field(default_factory=TargetCollection) + indexes: list[IndexModel] | None = None + docs: list[dict[str, Any]] | None = None + command: dict[str, Any] | Callable[[CommandContext], dict[str, Any]] | None = None + expected: dict[str, Any] | Callable[[CommandContext], dict[str, Any]] | None = None + + def prepare(self, db: Database, collection: Collection) -> Collection: + """Resolve the target collection and apply indexes/docs.""" + collection = self.target_collection.resolve(db, collection) + if self.indexes: + collection.create_indexes(self.indexes) + if self.docs: + collection.insert_many(self.docs) + return collection + + def build_command(self, ctx: CommandContext) -> dict[str, Any]: + """Resolve the command dict from a callable or plain dict.""" + if self.command is None: + raise ValueError(f"CommandTestCase '{self.id}' has no command defined") + if isinstance(self.command, dict): + return self.command + return self.command(ctx) + + def build_expected(self, ctx: CommandContext) -> dict[str, Any] | None: + """Resolve expected from a callable or plain value.""" + if self.expected is None or isinstance(self.expected, dict): + return self.expected + return self.expected(ctx) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/utils/target_collection.py b/documentdb_tests/compatibility/tests/core/collections/commands/utils/target_collection.py new file mode 100644 index 00000000..7d96df63 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/utils/target_collection.py @@ -0,0 +1,55 @@ +"""Collection target types for command tests. + +Each subclass describes a kind of collection a test needs and knows how +to create it from the fixture collection. All derived names use the +fixture name as a prefix to guarantee parallel-safe uniqueness. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from pymongo.collection import Collection +from pymongo.database import Database + + +@dataclass(frozen=True) +class TargetCollection: + """Default. Use the fixture collection as-is.""" + + def resolve(self, db: Database, collection: Collection) -> Collection: + return collection + + +@dataclass(frozen=True) +class ViewCollection(TargetCollection): + """A view on the fixture collection.""" + + def resolve(self, db: Database, collection: Collection) -> Collection: + view_name = f"{collection.name}_view" + db.command("create", view_name, viewOn=collection.name, pipeline=[]) + return db[view_name] + + +@dataclass(frozen=True) +class CappedCollection(TargetCollection): + """A capped collection.""" + + size: int = 4096 + + def resolve(self, db: Database, collection: Collection) -> Collection: + name = f"{collection.name}_capped" + db.create_collection(name, capped=True, size=self.size) + return db[name] + + +@dataclass(frozen=True) +class NamedCollection(TargetCollection): + """A collection with a custom name suffix.""" + + suffix: str = "" + + def resolve(self, db: Database, collection: Collection) -> Collection: + name = f"{collection.name}{self.suffix}" + db.create_collection(name) + return db[name] diff --git a/documentdb_tests/framework/assertions.py b/documentdb_tests/framework/assertions.py index c9528ac7..777f0447 100644 --- a/documentdb_tests/framework/assertions.py +++ b/documentdb_tests/framework/assertions.py @@ -239,6 +239,7 @@ def assertResult( msg: Optional[str] = None, ignore_order_in: Optional[list[str]] = None, ignore_doc_order: bool = False, + raw_res: bool = False, ): """ Universal assertion that handles success and error cases. @@ -252,11 +253,14 @@ def assertResult( comparison (for fields like set operation results where element order is unspecified) ignore_doc_order: If True, compare lists ignoring order (duplicates still matter) + raw_res: If True, compare the raw result dict instead of + extracting cursor.firstBatch Usage: assertResult(result, expected=[{"_id": 1}]) # Success case assertResult(result, error_code=16555) # Error case assertResult(result, expected=[{"r": [3, 1, 2]}], ignore_order_in=["r"]) + assertResult(result, expected={"ok": 1.0}, raw_res=True) # Raw command result """ if error_code is not None: assertFailureCode(result, error_code, msg) @@ -265,6 +269,7 @@ def assertResult( result, expected, msg, + raw_res=raw_res, ignore_order_in=ignore_order_in, ignore_doc_order=ignore_doc_order, ) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 67a764f6..f77454d7 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -7,6 +7,7 @@ FAILED_TO_PARSE_ERROR = 9 TYPE_MISMATCH_ERROR = 14 OVERFLOW_ERROR = 15 +INVALID_NAMESPACE_ERROR = 73 UNRECOGNIZED_EXPRESSION_ERROR = 168 CONVERSION_FAILURE_ERROR = 241 EXPRESSION_NOT_OBJECT_ERROR = 10065 @@ -176,6 +177,7 @@ ARRAY_TO_OBJECT_INVALID_PAIR_ERROR = 40397 ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR = 40398 MERGE_OBJECTS_INVALID_TYPE_ERROR = 40400 +UNRECOGNIZED_COMMAND_FIELD_ERROR = 40415 INVALID_TIMEZONE_ERROR = 40485 DATEFROMPARTS_MIXING_ERROR = 40489 DATEFROMPARTS_INVALID_TYPE_ERROR = 40515