diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_bson_type_validation.py b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_bson_type_validation.py new file mode 100644 index 000000000..cbc1ba6e1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_bson_type_validation.py @@ -0,0 +1,92 @@ +"""Tests for logRotate BSON type validation. + +The value field accepts numeric/bool types and the string "server"; other +types are rejected with TypeMismatch. The comment field accepts all types. +""" + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import FILE_RENAME_FAILED_ERROR, TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import ( + execute_admin_command, + execute_admin_with_retry_command, +) + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +LOG_ROTATE_VALUE_PARAMS = [ + BsonTypeTestCase( + id="logRotate_value", + msg="logRotate value should accept numeric, bool, and the 'server' string", + keyword="logRotate", + valid_types=[ + BsonType.INT, + BsonType.DOUBLE, + BsonType.LONG, + BsonType.BOOL, + BsonType.DECIMAL, + BsonType.STRING, + ], + valid_inputs={BsonType.STRING: "server"}, + default_error_code=TYPE_MISMATCH_ERROR, + ), +] + +COMMENT_PARAMS = [ + BsonTypeTestCase( + id="comment", + msg="logRotate should accept all BSON types for the comment field", + keyword="comment", + valid_types=list(BsonType), + ), +] + + +LOG_ROTATE_VALUE_ACCEPTANCE = generate_bson_acceptance_test_cases(LOG_ROTATE_VALUE_PARAMS) +LOG_ROTATE_VALUE_REJECTIONS = generate_bson_rejection_test_cases(LOG_ROTATE_VALUE_PARAMS) +COMMENT_ACCEPTANCE = generate_bson_acceptance_test_cases(COMMENT_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", LOG_ROTATE_VALUE_ACCEPTANCE) +def test_logRotate_value_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test logRotate accepts numeric, bool, and the 'server' string value.""" + result = execute_admin_with_retry_command( + collection, {"logRotate": sample_value}, retry_code=FILE_RENAME_FAILED_ERROR + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg=f"{spec.msg} (bson_type={bson_type.value})", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", COMMENT_ACCEPTANCE) +def test_logRotate_comment_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test comment field accepts all BSON types.""" + result = execute_admin_with_retry_command( + collection, {"logRotate": 1, "comment": sample_value}, retry_code=FILE_RENAME_FAILED_ERROR + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg=f"comment should accept {bson_type.value}", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", LOG_ROTATE_VALUE_REJECTIONS) +def test_logRotate_value_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test logRotate value rejects non-numeric, non-string BSON types.""" + result = execute_admin_command(collection, {"logRotate": sample_value}) + assertFailureCode( + result, + spec.expected_code(bson_type), + msg=f"logRotate should reject {bson_type.value} for the command value", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_errors.py new file mode 100644 index 000000000..d8e5299af --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_errors.py @@ -0,0 +1,74 @@ +"""Tests for logRotate command error cases.""" + +import pytest + +from documentdb_tests.compatibility.tests.system.administration.utils.admin_test_case import ( + AdminTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + NO_SUCH_KEY_ERROR, + UNAUTHORIZED_ERROR, + UNRECOGNIZED_COMMAND_FIELD_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.no_parallel] + + +INVALID_STRING_TESTS: list[AdminTestCase] = [ + AdminTestCase( + "invalid_string", + command={"logRotate": "invalid"}, + error_code=NO_SUCH_KEY_ERROR, + msg="Should reject invalid string value", + ), + AdminTestCase( + "empty_string", + command={"logRotate": ""}, + error_code=NO_SUCH_KEY_ERROR, + msg="Should reject empty string", + ), + AdminTestCase( + "case_sensitive_SERVER", + command={"logRotate": "SERVER"}, + error_code=NO_SUCH_KEY_ERROR, + msg="Should reject uppercase SERVER (case-sensitive)", + ), + AdminTestCase( + "case_sensitive_Audit", + command={"logRotate": "Audit"}, + error_code=NO_SUCH_KEY_ERROR, + msg="Should reject mixed-case Audit (case-sensitive)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_STRING_TESTS)) +def test_logRotate_invalid_arguments(collection, test): + """Test that logRotate rejects invalid string values.""" + result = execute_admin_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_logRotate_unrecognized_field(collection): + """Test that logRotate rejects unrecognized top-level fields.""" + result = execute_admin_command(collection, {"logRotate": 1, "unknownField": 1}) + assertFailureCode( + result, UNRECOGNIZED_COMMAND_FIELD_ERROR, msg="Should reject unrecognized fields" + ) + + +def test_logRotate_non_admin_database(collection): + """Test that logRotate fails when run on a non-admin database.""" + result = execute_command(collection, {"logRotate": 1}) + assertFailureCode(result, UNAUTHORIZED_ERROR, msg="Should fail on non-admin database") + + +def test_logRotate_audit_target_rejected_when_disabled(collection): + """Test that the 'audit' log target is rejected when auditing is disabled.""" + result = execute_admin_command(collection, {"logRotate": "audit"}) + assertFailureCode( + result, NO_SUCH_KEY_ERROR, msg="Should reject audit target when auditing is disabled" + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_value_acceptance.py b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_value_acceptance.py new file mode 100644 index 000000000..40b414a65 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_logRotate_value_acceptance.py @@ -0,0 +1,37 @@ +"""Tests for logRotate acceptance of specific scalar values. + +Covers values the shared BSON-type harness does not sample: boolean `false`, `0`, +and negative integers/longs (it only feeds `True` and `INT32_MAX`/`INT64_MAX`). +Each rotation goes through `execute_admin_with_retry_command`, which retries past the +transient FileRenameFailed so the test can assert a clean success. +""" + +import pytest +from bson.int64 import Int64 + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.error_codes import FILE_RENAME_FAILED_ERROR +from documentdb_tests.framework.executor import execute_admin_with_retry_command + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +def test_logRotate_value_bool_false_accepted(collection): + """Test logRotate accepts boolean false (the BSON sample only covers true).""" + result = execute_admin_with_retry_command( + collection, {"logRotate": False}, retry_code=FILE_RENAME_FAILED_ERROR + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="logRotate value should accept boolean false") + + +@pytest.mark.parametrize( + "value", + [0, -1, Int64(-5)], + ids=["zero", "negative_int", "negative_long"], +) +def test_logRotate_value_zero_and_negative_accepted(collection, value): + """Test logRotate accepts 0 and negative integers (BSON samples only cover max values).""" + result = execute_admin_with_retry_command( + collection, {"logRotate": value}, retry_code=FILE_RENAME_FAILED_ERROR + ) + assertSuccessPartial(result, {"ok": 1.0}, msg=f"logRotate value should accept {value!r}") diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_smoke_logRotate.py b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_smoke_logRotate.py index f3916594a..30f250b88 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_smoke_logRotate.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/logRotate/test_smoke_logRotate.py @@ -1,20 +1,17 @@ -""" -Smoke test for logRotate command. - -Tests basic logRotate functionality. -""" +"""Smoke test for logRotate command.""" import pytest from documentdb_tests.framework.assertions import assertSuccessPartial -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.error_codes import FILE_RENAME_FAILED_ERROR +from documentdb_tests.framework.executor import execute_admin_with_retry_command pytestmark = [pytest.mark.smoke, pytest.mark.no_parallel] def test_smoke_logRotate(collection): """Test basic logRotate behavior.""" - result = execute_admin_command(collection, {"logRotate": 1}) - - expected = {"ok": 1.0} - assertSuccessPartial(result, expected, msg="Should support logRotate command") + result = execute_admin_with_retry_command( + collection, {"logRotate": 1}, retry_code=FILE_RENAME_FAILED_ERROR + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="Should support logRotate command") diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py b/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/admin_test_case.py b/documentdb_tests/compatibility/tests/system/administration/utils/admin_test_case.py new file mode 100644 index 000000000..1780e1416 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/utils/admin_test_case.py @@ -0,0 +1,22 @@ +"""Shared test case for administration command tests.""" + +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class AdminTestCase(BaseTestCase): + """Test case for administration command tests. + + Inherits ``id``, ``expected``, ``error_code``, ``msg``, and ``marks`` from + ``BaseTestCase``. + + Attributes: + command: The command document to execute. + use_admin: If True, execute against the admin database. + """ + + command: Optional[Dict[str, Any]] = None + use_admin: bool = True diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 86884ce4c..ef3d8c6ee 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -15,6 +15,7 @@ NAMESPACE_NOT_FOUND_ERROR = 26 INDEX_NOT_FOUND_ERROR = 27 PATH_NOT_VIABLE_ERROR = 28 +FILE_RENAME_FAILED_ERROR = 37 CONFLICTING_UPDATE_OPERATORS_ERROR = 40 CURSOR_NOT_FOUND_ERROR = 43 NAMESPACE_EXISTS_ERROR = 48 diff --git a/documentdb_tests/framework/executor.py b/documentdb_tests/framework/executor.py index ba3779533..42eae6373 100644 --- a/documentdb_tests/framework/executor.py +++ b/documentdb_tests/framework/executor.py @@ -2,6 +2,7 @@ Unified execution and assertion utilities for tests. """ +import time from datetime import timezone from typing import Any, Dict @@ -49,3 +50,31 @@ def execute_admin_command(collection, command: Dict) -> Any: return result except Exception as e: return e + + +def execute_admin_with_retry_command( + collection, command: Dict, *, retry_code: int, timeout: float = 30.0, interval: float = 0.2 +) -> Any: + """ + Run an admin command, retrying while it fails with ``retry_code``. + + Any other result (success or a different error) is returned immediately. On + timeout, the last result is returned as-is. + + Args: + collection: DocumentDB collection + command: Command to execute via runCommand on the admin database + retry_code: Error code to treat as transient and retry past + timeout: Maximum seconds to keep retrying before returning the last result + interval: Seconds to wait between attempts + + Returns: + Result if successful, Exception if failed + """ + deadline = time.monotonic() + timeout + while True: + result = execute_admin_command(collection, command) + should_retry = isinstance(result, Exception) and getattr(result, "code", None) == retry_code + if not should_retry or time.monotonic() >= deadline: + return result + time.sleep(interval) diff --git a/documentdb_tests/framework/test_format_validator.py b/documentdb_tests/framework/test_format_validator.py index 295b1351d..4d224c66b 100644 --- a/documentdb_tests/framework/test_format_validator.py +++ b/documentdb_tests/framework/test_format_validator.py @@ -32,6 +32,7 @@ def validate_test_format(file_path: str) -> list[str]: "execute_project_with_insert", "execute_expression", "execute_expression_with_insert", + "execute_admin_with_retry_command", ] )