@@ -394,25 +394,47 @@ async def test_custom_table_name(
394394):
395395 """Test that the store works correctly with a custom table name."""
396396 table_name = 'my_custom_push_configs'
397+ engine = db_store_parameterized .engine
398+ custom_store = None
399+ try :
400+ # Use a new store with a custom table name
401+ custom_store = DatabasePushNotificationConfigStore (
402+ engine = engine ,
403+ create_table = True ,
404+ table_name = table_name ,
405+ encryption_key = Fernet .generate_key (),
406+ )
397407
398- task_id = 'custom-table-task'
399- config = PushNotificationConfig (id = 'config-1' , url = 'http://custom.url' )
408+ task_id = 'custom-table-task'
409+ config = PushNotificationConfig (id = 'config-1' , url = 'http://custom.url' )
400410
401- # This will create the table on first use
402- await db_store_parameterized .set_info (task_id , config )
403- retrieved_configs = await db_store_parameterized .get_info (task_id )
411+ # This will create the table on first use
412+ await custom_store .set_info (task_id , config )
413+ retrieved_configs = await custom_store .get_info (task_id )
404414
405- assert len (retrieved_configs ) == 1
406- assert retrieved_configs [0 ] == config
415+ assert len (retrieved_configs ) == 1
416+ assert retrieved_configs [0 ] == config
407417
408- # Verify the custom table exists and has data
409- async with db_store_parameterized .engine .connect () as conn :
410- result = await conn .execute (
411- select (db_store_parameterized .config_model ).where (
412- db_store_parameterized .config_model .task_id == task_id
418+ # Verify the custom table exists and has data
419+ async with custom_store .engine .connect () as conn :
420+
421+ def has_table_sync (sync_conn ):
422+ inspector = inspect (sync_conn )
423+ return inspector .has_table (table_name )
424+
425+ assert await conn .run_sync (has_table_sync )
426+
427+ result = await conn .execute (
428+ select (custom_store .config_model ).where (
429+ custom_store .config_model .task_id == task_id
430+ )
413431 )
414- )
415- assert result .scalar_one_or_none () is not None
432+ assert result .scalar_one_or_none () is not None
433+ finally :
434+ if custom_store :
435+ # Clean up the dynamically created table from the metadata
436+ # to prevent errors in subsequent parameterized test runs.
437+ Base .metadata .remove (custom_store .config_model .__table__ ) # type: ignore
416438
417439
418440@pytest .mark .asyncio
@@ -432,9 +454,9 @@ async def test_set_and_get_info_multiple_configs_no_key(
432454 config1 = PushNotificationConfig (id = 'config-1' , url = 'http://example.com/1' )
433455 config2 = PushNotificationConfig (id = 'config-2' , url = 'http://example.com/2' )
434456
435- await db_store_parameterized .set_info (task_id , config1 )
436- await db_store_parameterized .set_info (task_id , config2 )
437- retrieved_configs = await db_store_parameterized .get_info (task_id )
457+ await store .set_info (task_id , config1 )
458+ await store .set_info (task_id , config2 )
459+ retrieved_configs = await store .get_info (task_id )
438460
439461 assert len (retrieved_configs ) == 2
440462 assert config1 in retrieved_configs
@@ -472,3 +494,69 @@ async def test_data_is_not_encrypted_in_db_if_no_key_is_set(
472494 db_model = result .scalar_one ()
473495
474496 assert db_model .config_data == plain_json .encode ('utf-8' )
497+
498+
499+ @pytest .mark .asyncio
500+ async def test_decryption_fallback_for_unencrypted_data (
501+ db_store_parameterized : DatabasePushNotificationConfigStore ,
502+ ):
503+ """Test reading unencrypted data with an encryption-enabled store."""
504+ # 1. Store unencrypted data using a new store instance without a key
505+ unencrypted_store = DatabasePushNotificationConfigStore (
506+ engine = db_store_parameterized .engine ,
507+ create_table = False , # Table already exists from fixture
508+ encryption_key = None ,
509+ )
510+ await unencrypted_store .initialize ()
511+
512+ task_id = 'mixed-encryption-task'
513+ config = PushNotificationConfig (id = 'config-1' , url = 'http://plain.url' )
514+ await unencrypted_store .set_info (task_id , config )
515+
516+ # 2. Try to read with the encryption-enabled store from the fixture
517+ retrieved_configs = await db_store_parameterized .get_info (task_id )
518+
519+ # Should fall back to parsing as plain JSON and not fail
520+ assert len (retrieved_configs ) == 1
521+ assert retrieved_configs [0 ] == config
522+
523+
524+ @pytest .mark .asyncio
525+ async def test_parsing_error_after_successful_decryption (
526+ db_store_parameterized : DatabasePushNotificationConfigStore ,
527+ ):
528+ """Test that a parsing error after successful decryption is handled."""
529+
530+ task_id = 'corrupted-data-task'
531+ config_id = 'config-1'
532+
533+ # 1. Encrypt data that is NOT valid JSON
534+ fernet = Fernet (Fernet .generate_key ())
535+ corrupted_payload = b'this is not valid json'
536+ encrypted_data = fernet .encrypt (corrupted_payload )
537+
538+ # 2. Manually insert this corrupted data into the DB
539+ async_session = async_sessionmaker (
540+ db_store_parameterized .engine , expire_on_commit = False
541+ )
542+ async with async_session () as session :
543+ db_model = PushNotificationConfigModel (
544+ task_id = task_id ,
545+ config_id = config_id ,
546+ config_data = encrypted_data ,
547+ )
548+ session .add (db_model )
549+ await session .commit ()
550+
551+ # 3. get_info should log an error and return an empty list
552+ retrieved_configs = await db_store_parameterized .get_info (task_id )
553+ assert retrieved_configs == []
554+
555+ # 4. _from_orm should raise a ValueError
556+ async with async_session () as session :
557+ db_model_retrieved = await session .get (
558+ PushNotificationConfigModel , (task_id , config_id )
559+ )
560+
561+ with pytest .raises (ValueError ) as exc_info :
562+ db_store_parameterized ._from_orm (db_model_retrieved ) # type: ignore
0 commit comments