@@ -44,19 +44,19 @@ class MongoDBFailedToStartError(Exception):
4444 pass
4545
4646
47- def test_managed_entry_document_conversion ():
47+ def test_managed_entry_document_conversion_native_mode ():
4848 created_at = datetime (year = 2025 , month = 1 , day = 1 , hour = 0 , minute = 0 , second = 0 , tzinfo = timezone .utc )
4949 expires_at = created_at + timedelta (seconds = 10 )
5050
5151 managed_entry = ManagedEntry (value = {"test" : "test" }, created_at = created_at , expires_at = expires_at )
52- document = managed_entry_to_document (key = "test" , managed_entry = managed_entry )
52+ document = managed_entry_to_document (key = "test" , managed_entry = managed_entry , native_storage = True )
5353
5454 assert document == snapshot (
5555 {
5656 "key" : "test" ,
57- "value" : '{" test": "test"}' ,
58- "created_at" : " 2025-01-01T00:00:00+00:00" ,
59- "expires_at" : " 2025-01-01T00:00:10+00:00" ,
57+ "value" : { "dict" : { " test" : "test" }} ,
58+ "created_at" : datetime ( 2025 , 1 , 1 , 0 , 0 , tzinfo = timezone . utc ) ,
59+ "expires_at" : datetime ( 2025 , 1 , 1 , 0 , 0 , 10 , tzinfo = timezone . utc ) ,
6060 }
6161 )
6262
@@ -68,8 +68,55 @@ def test_managed_entry_document_conversion():
6868 assert round_trip_managed_entry .expires_at == expires_at
6969
7070
71+ def test_managed_entry_document_conversion_legacy_mode ():
72+ created_at = datetime (year = 2025 , month = 1 , day = 1 , hour = 0 , minute = 0 , second = 0 , tzinfo = timezone .utc )
73+ expires_at = created_at + timedelta (seconds = 10 )
74+
75+ managed_entry = ManagedEntry (value = {"test" : "test" }, created_at = created_at , expires_at = expires_at )
76+ document = managed_entry_to_document (key = "test" , managed_entry = managed_entry , native_storage = False )
77+
78+ assert document == snapshot (
79+ {
80+ "key" : "test" ,
81+ "value" : {"string" : '{"test": "test"}' },
82+ "created_at" : datetime (2025 , 1 , 1 , 0 , 0 , tzinfo = timezone .utc ),
83+ "expires_at" : datetime (2025 , 1 , 1 , 0 , 0 , 10 , tzinfo = timezone .utc ),
84+ }
85+ )
86+
87+ round_trip_managed_entry = document_to_managed_entry (document = document )
88+
89+ assert round_trip_managed_entry .value == managed_entry .value
90+ assert round_trip_managed_entry .created_at == created_at
91+ assert round_trip_managed_entry .ttl == IsFloat (lt = 0 )
92+ assert round_trip_managed_entry .expires_at == expires_at
93+
94+
95+ def test_managed_entry_document_conversion_old_format ():
96+ """Test backward compatibility with old format where value is directly a string."""
97+ created_at = datetime (year = 2025 , month = 1 , day = 1 , hour = 0 , minute = 0 , second = 0 , tzinfo = timezone .utc )
98+ expires_at = created_at + timedelta (seconds = 10 )
99+
100+ # Simulate old document format
101+ old_document = {
102+ "key" : "test" ,
103+ "value" : '{"test": "test"}' ,
104+ "created_at" : "2025-01-01T00:00:00+00:00" ,
105+ "expires_at" : "2025-01-01T00:00:10+00:00" ,
106+ }
107+
108+ round_trip_managed_entry = document_to_managed_entry (document = old_document )
109+
110+ assert round_trip_managed_entry .value == {"test" : "test" }
111+ assert round_trip_managed_entry .created_at == created_at
112+ assert round_trip_managed_entry .ttl == IsFloat (lt = 0 )
113+ assert round_trip_managed_entry .expires_at == expires_at
114+
115+
71116@pytest .mark .skipif (should_skip_docker_tests (), reason = "Docker is not available" )
72117class TestMongoDBStore (ContextManagerStoreTestMixin , BaseStoreTests ):
118+ """Test MongoDBStore with native_storage=False (legacy mode) for backward compatibility."""
119+
73120 @pytest .fixture (autouse = True , scope = "session" , params = MONGODB_VERSIONS_TO_TEST )
74121 async def setup_mongodb (self , request : pytest .FixtureRequest ) -> AsyncGenerator [None , None ]:
75122 version = request .param
@@ -84,7 +131,8 @@ async def setup_mongodb(self, request: pytest.FixtureRequest) -> AsyncGenerator[
84131 @override
85132 @pytest .fixture
86133 async def store (self , setup_mongodb : None ) -> MongoDBStore :
87- store = MongoDBStore (url = f"mongodb://{ MONGODB_HOST } :{ MONGODB_HOST_PORT } " , db_name = MONGODB_TEST_DB )
134+ # Use legacy mode (native_storage=False) to test backward compatibility
135+ store = MongoDBStore (url = f"mongodb://{ MONGODB_HOST } :{ MONGODB_HOST_PORT } " , db_name = MONGODB_TEST_DB , native_storage = False )
88136 # Ensure a clean db by dropping our default test collection if it exists
89137 with contextlib .suppress (Exception ):
90138 _ = await store ._client .drop_database (name_or_database = MONGODB_TEST_DB ) # pyright: ignore[reportPrivateUsage]
@@ -106,3 +154,89 @@ async def test_mongodb_collection_name_sanitization(self, mongodb_store: MongoDB
106154
107155 collections = await mongodb_store .collections ()
108156 assert collections == snapshot (["test_collection_-daf4a2ec" ])
157+
158+
159+ @pytest .mark .skipif (should_skip_docker_tests (), reason = "Docker is not available" )
160+ class TestMongoDBStoreNativeMode (ContextManagerStoreTestMixin , BaseStoreTests ):
161+ """Test MongoDBStore with native_storage=True (default)."""
162+
163+ @pytest .fixture (autouse = True , scope = "session" , params = MONGODB_VERSIONS_TO_TEST )
164+ async def setup_mongodb (self , request : pytest .FixtureRequest ) -> AsyncGenerator [None , None ]:
165+ version = request .param
166+
167+ with docker_container (f"mongodb-test-native-{ version } " , f"mongo:{ version } " , {str (MONGODB_HOST_PORT ): MONGODB_HOST_PORT }):
168+ if not await async_wait_for_true (bool_fn = ping_mongodb , tries = WAIT_FOR_MONGODB_TIMEOUT , wait_time = 1 ):
169+ msg = f"MongoDB { version } failed to start"
170+ raise MongoDBFailedToStartError (msg )
171+
172+ yield
173+
174+ @override
175+ @pytest .fixture
176+ async def store (self , setup_mongodb : None ) -> MongoDBStore :
177+ store = MongoDBStore (url = f"mongodb://{ MONGODB_HOST } :{ MONGODB_HOST_PORT } " , db_name = f"{ MONGODB_TEST_DB } -native" , native_storage = True )
178+ # Ensure a clean db by dropping our default test collection if it exists
179+ with contextlib .suppress (Exception ):
180+ _ = await store ._client .drop_database (name_or_database = f"{ MONGODB_TEST_DB } -native" ) # pyright: ignore[reportPrivateUsage]
181+
182+ return store
183+
184+ @pytest .fixture
185+ async def mongodb_store (self , store : MongoDBStore ) -> MongoDBStore :
186+ return store
187+
188+ @pytest .mark .skip (reason = "Distributed Caches are unbounded" )
189+ @override
190+ async def test_not_unbounded (self , store : BaseStore ): ...
191+
192+ async def test_value_stored_as_bson_dict (self , mongodb_store : MongoDBStore ):
193+ """Verify values are stored as BSON dicts, not JSON strings."""
194+ await mongodb_store .put (collection = "test" , key = "test_key" , value = {"name" : "Alice" , "age" : 30 })
195+
196+ # Get the raw MongoDB document
197+ await mongodb_store ._setup_collection (collection = "test" ) # pyright: ignore[reportPrivateUsage]
198+ sanitized_collection = mongodb_store ._sanitize_collection_name (collection = "test" ) # pyright: ignore[reportPrivateUsage]
199+ collection = mongodb_store ._collections_by_name [sanitized_collection ] # pyright: ignore[reportPrivateUsage]
200+ doc = await collection .find_one ({"key" : "test_key" })
201+
202+ # In native mode, value should be a dict with "dict" subfield
203+ assert doc is not None
204+ assert isinstance (doc ["value" ], dict )
205+ assert "dict" in doc ["value" ]
206+ assert doc ["value" ]["dict" ] == {"name" : "Alice" , "age" : 30 }
207+
208+ async def test_migration_from_legacy_mode (self , mongodb_store : MongoDBStore ):
209+ """Verify native mode can read legacy JSON string data."""
210+ # Manually insert a legacy document with JSON string value in the new format
211+ await mongodb_store ._setup_collection (collection = "test" ) # pyright: ignore[reportPrivateUsage]
212+ sanitized_collection = mongodb_store ._sanitize_collection_name (collection = "test" ) # pyright: ignore[reportPrivateUsage]
213+ collection = mongodb_store ._collections_by_name [sanitized_collection ] # pyright: ignore[reportPrivateUsage]
214+
215+ await collection .insert_one (
216+ {
217+ "key" : "legacy_key" ,
218+ "value" : {"string" : '{"legacy": "data"}' }, # New format with JSON string
219+ }
220+ )
221+
222+ # Should be able to read it in native mode
223+ result = await mongodb_store .get (collection = "test" , key = "legacy_key" )
224+ assert result == {"legacy" : "data" }
225+
226+ async def test_migration_from_old_format (self , mongodb_store : MongoDBStore ):
227+ """Verify native mode can read old format where value is directly a string."""
228+ # Manually insert an old document with value directly as JSON string
229+ await mongodb_store ._setup_collection (collection = "test" ) # pyright: ignore[reportPrivateUsage]
230+ sanitized_collection = mongodb_store ._sanitize_collection_name (collection = "test" ) # pyright: ignore[reportPrivateUsage]
231+ collection = mongodb_store ._collections_by_name [sanitized_collection ] # pyright: ignore[reportPrivateUsage]
232+
233+ await collection .insert_one (
234+ {
235+ "key" : "old_key" ,
236+ "value" : '{"old": "format"}' , # Old format: value directly as JSON string
237+ }
238+ )
239+
240+ # Should be able to read it in native mode
241+ result = await mongodb_store .get (collection = "test" , key = "old_key" )
242+ assert result == {"old" : "format" }
0 commit comments