Skip to content

Commit fb36e40

Browse files
committed
mongo test cleanup
1 parent 4aa6f73 commit fb36e40

File tree

6 files changed

+167
-138
lines changed

6 files changed

+167
-138
lines changed

key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def document_to_managed_entry(document: dict[str, Any]) -> ManagedEntry:
7070
msg = "Expected `created_at` field to be a datetime"
7171
raise DeserializationError(msg)
7272
data["created_at"] = created_at_datetime.replace(tzinfo=timezone.utc)
73+
7374
if expires_at_datetime := document.get("expires_at"):
7475
if not isinstance(expires_at_datetime, datetime):
7576
msg = "Expected `expires_at` field to be a datetime"
@@ -104,11 +105,17 @@ def managed_entry_to_document(key: str, managed_entry: ManagedEntry, *, native_s
104105
"""
105106
document: dict[str, Any] = {"key": key, "value": {}}
106107

108+
# We convert to JSON even if we don't need to, this ensures that the value we were provided
109+
# can be serialized to JSON which helps ensure compatibility across stores. For example,
110+
# Mongo can natively handle datetime objects which other stores cannot, if we don't convert to JSON,
111+
# then using py-key-value with Mongo will return different values than if we used another store.
112+
json_str = managed_entry.value_as_json
113+
107114
# Store in appropriate field based on mode
108115
if native_storage:
109116
document["value"]["object"] = managed_entry.value_as_dict
110117
else:
111-
document["value"]["string"] = managed_entry.value_as_json
118+
document["value"]["string"] = json_str
112119

113120
# Add metadata fields
114121
if managed_entry.created_at:

key-value/key-value-aio/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def docker_container(
168168
finally:
169169
docker_stop(name, raise_on_error=False)
170170
docker_rm(name, raise_on_error=False)
171+
docker_wait_container_gone(name=name, max_tries=10, wait_time=1.0)
171172

172173
logger.info(f"Container {name} stopped and removed")
173174
return

key-value/key-value-aio/tests/stores/mongodb/test_mongodb.py

Lines changed: 75 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
from typing import Any
55

66
import pytest
7-
from dirty_equals import IsFloat
7+
from dirty_equals import IsDatetime, IsFloat, IsInstance
88
from inline_snapshot import snapshot
99
from key_value.shared.stores.wait import async_wait_for_true
1010
from key_value.shared.utils.managed_entry import ManagedEntry
1111
from pymongo import AsyncMongoClient
12+
from pymongo.collection import ObjectId
1213
from typing_extensions import override
1314

1415
from key_value.aio.stores.base import BaseStore
@@ -92,9 +93,13 @@ def test_managed_entry_document_conversion_legacy_mode():
9293
assert round_trip_managed_entry.expires_at == expires_at
9394

9495

95-
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
96-
class TestMongoDBStore(ContextManagerStoreTestMixin, BaseStoreTests):
97-
"""Test MongoDBStore with native_storage=False (legacy mode) for backward compatibility."""
96+
async def clean_mongodb_database(store: MongoDBStore) -> None:
97+
with contextlib.suppress(Exception):
98+
_ = await store._client.drop_database(name_or_database=MONGODB_TEST_DB) # pyright: ignore[reportPrivateUsage]
99+
100+
101+
class BaseMongoDBStoreTests(ContextManagerStoreTestMixin, BaseStoreTests):
102+
"""Base class for MongoDB store tests."""
98103

99104
@pytest.fixture(autouse=True, scope="session", params=MONGODB_VERSIONS_TO_TEST)
100105
async def setup_mongodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
@@ -107,49 +112,23 @@ async def setup_mongodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[
107112

108113
yield
109114

110-
@override
111-
@pytest.fixture
112-
async def store(self, setup_mongodb: None) -> MongoDBStore:
113-
# Use legacy mode (native_storage=False) to test backward compatibility
114-
store = MongoDBStore(url=f"mongodb://{MONGODB_HOST}:{MONGODB_HOST_PORT}", db_name=MONGODB_TEST_DB, native_storage=False)
115-
# Ensure a clean db by dropping our default test collection if it exists
116-
with contextlib.suppress(Exception):
117-
_ = await store._client.drop_database(name_or_database=MONGODB_TEST_DB) # pyright: ignore[reportPrivateUsage]
118-
119-
return store
120-
121-
@pytest.fixture
122-
async def mongodb_store(self, store: MongoDBStore) -> MongoDBStore:
123-
return store
124-
125115
@pytest.mark.skip(reason="Distributed Caches are unbounded")
126116
@override
127117
async def test_not_unbounded(self, store: BaseStore): ...
128118

129-
async def test_mongodb_collection_name_sanitization(self, mongodb_store: MongoDBStore):
119+
async def test_mongodb_collection_name_sanitization(self, store: MongoDBStore):
130120
"""Tests that a special characters in the collection name will not raise an error."""
131-
await mongodb_store.put(collection="test_collection!@#$%^&*()", key="test_key", value={"test": "test"})
132-
assert await mongodb_store.get(collection="test_collection!@#$%^&*()", key="test_key") == {"test": "test"}
121+
await store.put(collection="test_collection!@#$%^&*()", key="test_key", value={"test": "test"})
122+
assert await store.get(collection="test_collection!@#$%^&*()", key="test_key") == {"test": "test"}
133123

134-
collections = await mongodb_store.collections()
124+
collections = await store.collections()
135125
assert collections == snapshot(["test_collection_-daf4a2ec"])
136126

137127

138128
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
139-
class TestMongoDBStoreNativeMode(ContextManagerStoreTestMixin, BaseStoreTests):
129+
class TestMongoDBStoreNativeMode(BaseMongoDBStoreTests):
140130
"""Test MongoDBStore with native_storage=True (default)."""
141131

142-
@pytest.fixture(autouse=True, scope="session", params=MONGODB_VERSIONS_TO_TEST)
143-
async def setup_mongodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[None, None]:
144-
version = request.param
145-
146-
with docker_container(f"mongodb-test-native-{version}", f"mongo:{version}", {str(MONGODB_HOST_PORT): MONGODB_HOST_PORT}):
147-
if not await async_wait_for_true(bool_fn=ping_mongodb, tries=WAIT_FOR_MONGODB_TIMEOUT, wait_time=1):
148-
msg = f"MongoDB {version} failed to start"
149-
raise MongoDBFailedToStartError(msg)
150-
151-
yield
152-
153132
@override
154133
@pytest.fixture
155134
async def store(self, setup_mongodb: None) -> MongoDBStore:
@@ -160,36 +139,31 @@ async def store(self, setup_mongodb: None) -> MongoDBStore:
160139

161140
return store
162141

163-
@pytest.fixture
164-
async def mongodb_store(self, store: MongoDBStore) -> MongoDBStore:
165-
return store
166-
167-
@pytest.mark.skip(reason="Distributed Caches are unbounded")
168-
@override
169-
async def test_not_unbounded(self, store: BaseStore): ...
170-
171-
async def test_value_stored_as_bson_dict(self, mongodb_store: MongoDBStore):
142+
async def test_value_stored_as_bson_dict(self, store: MongoDBStore):
172143
"""Verify values are stored as BSON dicts, not JSON strings."""
173-
await mongodb_store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30})
144+
await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30})
174145

175146
# Get the raw MongoDB document
176-
await mongodb_store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
177-
sanitized_collection = mongodb_store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
178-
collection = mongodb_store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
147+
await store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
148+
sanitized_collection = store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
149+
collection = store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
179150
doc = await collection.find_one({"key": "test_key"})
180151

181-
# In native mode, value should be a dict with "dict" subfield
182-
assert doc is not None
183-
assert isinstance(doc["value"], dict)
184-
assert "dict" in doc["value"]
185-
assert doc["value"]["dict"] == {"name": "Alice", "age": 30}
152+
assert doc == snapshot(
153+
{
154+
"_id": IsInstance(expected_type=ObjectId),
155+
"key": "test_key",
156+
"created_at": IsDatetime(),
157+
"value": {"object": {"name": "Alice", "age": 30}},
158+
}
159+
)
186160

187-
async def test_migration_from_legacy_mode(self, mongodb_store: MongoDBStore):
161+
async def test_migration_from_legacy_mode(self, store: MongoDBStore):
188162
"""Verify native mode can read legacy JSON string data."""
189163
# Manually insert a legacy document with JSON string value in the new format
190-
await mongodb_store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
191-
sanitized_collection = mongodb_store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
192-
collection = mongodb_store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
164+
await store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
165+
sanitized_collection = store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
166+
collection = store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
193167

194168
await collection.insert_one(
195169
{
@@ -199,23 +173,56 @@ async def test_migration_from_legacy_mode(self, mongodb_store: MongoDBStore):
199173
)
200174

201175
# Should be able to read it in native mode
202-
result = await mongodb_store.get(collection="test", key="legacy_key")
176+
result = await store.get(collection="test", key="legacy_key")
203177
assert result == {"legacy": "data"}
204178

205-
async def test_migration_from_old_format(self, mongodb_store: MongoDBStore):
206-
"""Verify native mode can read old format where value is directly a string."""
207-
# Manually insert an old document with value directly as JSON string
208-
await mongodb_store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
209-
sanitized_collection = mongodb_store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
210-
collection = mongodb_store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
179+
180+
@pytest.mark.skipif(should_skip_docker_tests(), reason="Docker is not available")
181+
class TestMongoDBStoreNonNativeMode(BaseMongoDBStoreTests):
182+
"""Test MongoDBStore with native_storage=False (legacy mode) for backward compatibility."""
183+
184+
@override
185+
@pytest.fixture
186+
async def store(self, setup_mongodb: None) -> MongoDBStore:
187+
store = MongoDBStore(url=f"mongodb://{MONGODB_HOST}:{MONGODB_HOST_PORT}", db_name=MONGODB_TEST_DB, native_storage=False)
188+
189+
await clean_mongodb_database(store=store)
190+
191+
return store
192+
193+
async def test_value_stored_as_json(self, store: MongoDBStore):
194+
"""Verify values are stored as JSON strings."""
195+
await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30})
196+
197+
# Get the raw MongoDB document
198+
await store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
199+
sanitized_collection = store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
200+
collection = store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
201+
doc = await collection.find_one({"key": "test_key"})
202+
203+
assert doc == snapshot(
204+
{
205+
"_id": IsInstance(expected_type=ObjectId),
206+
"key": "test_key",
207+
"created_at": IsDatetime(),
208+
"value": {"string": '{"age": 30, "name": "Alice"}'},
209+
}
210+
)
211+
212+
async def test_migration_from_native_mode(self, store: MongoDBStore):
213+
"""Verify native mode can read native mode JSON string data."""
214+
# Manually insert a legacy document with JSON string value in the new format
215+
await store._setup_collection(collection="test") # pyright: ignore[reportPrivateUsage]
216+
sanitized_collection = store._sanitize_collection_name(collection="test") # pyright: ignore[reportPrivateUsage]
217+
collection = store._collections_by_name[sanitized_collection] # pyright: ignore[reportPrivateUsage]
211218

212219
await collection.insert_one(
213220
{
214-
"key": "old_key",
215-
"value": '{"old": "format"}', # Old format: value directly as JSON string
221+
"key": "legacy_key",
222+
"value": {"object": {"name": "Alice", "age": 30}},
216223
}
217224
)
218225

219226
# Should be able to read it in native mode
220-
result = await mongodb_store.get(collection="test", key="old_key")
221-
assert result == {"old": "format"}
227+
result = await store.get(collection="test", key="legacy_key")
228+
assert result == {"name": "Alice", "age": 30}

key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/store.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def document_to_managed_entry(document: dict[str, Any]) -> ManagedEntry:
7777
msg = "Expected `created_at` field to be a datetime"
7878
raise DeserializationError(msg)
7979
data["created_at"] = created_at_datetime.replace(tzinfo=timezone.utc)
80+
8081
if expires_at_datetime := document.get("expires_at"):
8182
if not isinstance(expires_at_datetime, datetime):
8283
msg = "Expected `expires_at` field to be a datetime"
@@ -111,11 +112,17 @@ def managed_entry_to_document(key: str, managed_entry: ManagedEntry, *, native_s
111112
"""
112113
document: dict[str, Any] = {"key": key, "value": {}}
113114

115+
# We convert to JSON even if we don't need to, this ensures that the value we were provided
116+
# can be serialized to JSON which helps ensure compatibility across stores. For example,
117+
# Mongo can natively handle datetime objects which other stores cannot, if we don't convert to JSON,
118+
# then using py-key-value with Mongo will return different values than if we used another store.
119+
json_str = managed_entry.value_as_json
120+
114121
# Store in appropriate field based on mode
115122
if native_storage:
116123
document["value"]["object"] = managed_entry.value_as_dict
117124
else:
118-
document["value"]["string"] = managed_entry.value_as_json
125+
document["value"]["string"] = json_str
119126

120127
# Add metadata fields
121128
if managed_entry.created_at:

key-value/key-value-sync/tests/code_gen/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def docker_container(
170170
finally:
171171
docker_stop(name, raise_on_error=False)
172172
docker_rm(name, raise_on_error=False)
173+
docker_wait_container_gone(name=name, max_tries=10, wait_time=1.0)
173174

174175
logger.info(f"Container {name} stopped and removed")
175176
return

0 commit comments

Comments
 (0)