@@ -560,3 +560,370 @@ def test_file_based_cache_read_data_json_invalid_encrypted_format(
560560 # Test reading invalid encrypted format
561561 result = file_based_cache ._read_data_json (test_file_path , encrypter_with_key )
562562 assert result == {}
563+
564+
565+ def test_file_based_cache_delete_method (file_based_cache , encrypter_with_key ):
566+ """Test FileBasedCache delete method removes data from both memory and file."""
567+ # Create test data
568+ sample_key = SecureCacheKey (["delete" , "test" ], "test_secret" )
569+ sample_data = ConnectionInfo (id = "test_delete_connection" , token = "test_token" )
570+
571+ # Set data in cache (both memory and file)
572+ file_based_cache .set (sample_key , sample_data )
573+
574+ # Verify data exists in memory cache
575+ memory_result = file_based_cache .memory_cache .get (sample_key )
576+ assert memory_result is not None
577+ assert memory_result .id == "test_delete_connection"
578+
579+ # Verify file exists on disk
580+ file_path = file_based_cache ._get_file_path (sample_key )
581+ assert os .path .exists (file_path )
582+
583+ # Delete the data
584+ file_based_cache .delete (sample_key )
585+
586+ # Verify data is removed from memory cache
587+ memory_result_after_delete = file_based_cache .memory_cache .get (sample_key )
588+ assert memory_result_after_delete is None
589+
590+ # Verify file is removed from disk
591+ assert not os .path .exists (file_path )
592+
593+ # Verify get returns None
594+ cache_result = file_based_cache .get (sample_key )
595+ assert cache_result is None
596+
597+
598+ @mark .nofakefs
599+ def test_file_based_cache_delete_method_file_removal_failure (
600+ file_based_cache , encrypter_with_key
601+ ):
602+ """Test FileBasedCache delete method handles file removal failures gracefully."""
603+ sample_key = SecureCacheKey (["delete" , "failure" ], "test_secret" )
604+ sample_data = ConnectionInfo (id = "test_connection" , token = "test_token" )
605+
606+ # Set data in memory cache only (no file operations due to @mark.nofakefs)
607+ file_based_cache .memory_cache .set (sample_key , sample_data )
608+
609+ # Mock path.exists to return True and os.remove to raise OSError
610+ with patch ("firebolt.utils.cache.path.exists" , return_value = True ), patch (
611+ "firebolt.utils.cache.os.remove"
612+ ) as mock_remove :
613+ mock_remove .side_effect = OSError ("Permission denied" )
614+
615+ # Delete should not raise an exception despite file removal failure
616+ file_based_cache .delete (sample_key )
617+
618+ # Verify data is still removed from memory cache
619+ memory_result = file_based_cache .memory_cache .get (sample_key )
620+ assert memory_result is None
621+
622+
623+ def test_file_based_cache_get_from_file_when_not_in_memory (
624+ file_based_cache , encrypter_with_key
625+ ):
626+ """Test FileBasedCache get method retrieves data from file when not in memory."""
627+ # Create test data
628+ sample_key = SecureCacheKey (["file" , "only" ], "test_secret" )
629+ sample_data = ConnectionInfo (
630+ id = "test_file_connection" ,
631+ token = "test_file_token" ,
632+ expiry_time = int (time .time ()) + 3600 , # Valid for 1 hour
633+ )
634+
635+ # First set data in cache (both memory and file)
636+ file_based_cache .set (sample_key , sample_data )
637+
638+ # Verify data exists
639+ initial_result = file_based_cache .get (sample_key )
640+ assert initial_result is not None
641+ assert initial_result .id == "test_file_connection"
642+
643+ # Clear memory cache but keep file
644+ file_based_cache .memory_cache .clear ()
645+
646+ # Verify memory cache is empty
647+ memory_result = file_based_cache .memory_cache .get (sample_key )
648+ assert memory_result is None
649+
650+ # Verify file still exists
651+ file_path = file_based_cache ._get_file_path (sample_key )
652+ assert os .path .exists (file_path )
653+
654+ # Get should retrieve from file and reload into memory
655+ file_result = file_based_cache .get (sample_key )
656+ assert file_result is not None
657+ assert file_result .id == "test_file_connection"
658+ assert file_result .token == "test_file_token"
659+
660+ # Verify data is now back in memory cache
661+ memory_result_after_load = file_based_cache .memory_cache .get (sample_key )
662+ assert memory_result_after_load is not None
663+ assert memory_result_after_load .id == "test_file_connection"
664+
665+
666+ def test_file_based_cache_get_from_corrupted_file (file_based_cache , encrypter_with_key ):
667+ """Test FileBasedCache get method handles corrupted file gracefully."""
668+ sample_key = SecureCacheKey (["corrupted" , "file" ], "test_secret" )
669+
670+ # Create corrupted file manually
671+ file_path = file_based_cache ._get_file_path (sample_key )
672+ os .makedirs (os .path .dirname (file_path ), exist_ok = True )
673+
674+ with open (file_path , "w" ) as f :
675+ f .write ("corrupted_data_that_cannot_be_decrypted" )
676+
677+ # Verify file exists
678+ assert os .path .exists (file_path )
679+
680+ # Get should return None due to decryption failure
681+ result = file_based_cache .get (sample_key )
682+ assert result is None
683+
684+ # Verify nothing is loaded into memory cache
685+ memory_result = file_based_cache .memory_cache .get (sample_key )
686+ assert memory_result is None
687+
688+
689+ def test_file_based_cache_disabled_behavior (file_based_cache , encrypter_with_key ):
690+ """Test FileBasedCache methods when cache is disabled."""
691+ sample_key = SecureCacheKey (["disabled" , "test" ], "test_secret" )
692+ sample_data = ConnectionInfo (id = "test_connection" , token = "test_token" )
693+
694+ # Disable the cache
695+ file_based_cache .disable ()
696+
697+ # Set should do nothing when disabled
698+ file_based_cache .set (sample_key , sample_data )
699+
700+ # Get should return None when disabled
701+ result = file_based_cache .get (sample_key )
702+ assert result is None
703+
704+ # Enable cache, set data, then disable again
705+ file_based_cache .enable ()
706+ file_based_cache .set (sample_key , sample_data )
707+
708+ # Verify data is set
709+ enabled_result = file_based_cache .get (sample_key )
710+ assert enabled_result is not None
711+
712+ # Disable and verify get returns None
713+ file_based_cache .disable ()
714+ disabled_result = file_based_cache .get (sample_key )
715+ assert disabled_result is None
716+
717+ # Delete should do nothing when disabled
718+ file_based_cache .delete (sample_key ) # Should not raise exception
719+
720+
721+ def test_file_based_cache_preserves_expiry_from_file (
722+ file_based_cache , encrypter_with_key , fixed_time
723+ ):
724+ """Test that FileBasedCache preserves original expiry time when loading from file."""
725+ sample_key = SecureCacheKey (["preserve" , "expiry" ], "test_secret" )
726+
727+ # Create data and set it at an earlier time
728+ sample_data = ConnectionInfo (id = "test_connection" )
729+
730+ # Set data at fixed_time - this will give it expiry of fixed_time + CACHE_EXPIRY_SECONDS
731+ with patch ("time.time" , return_value = fixed_time ):
732+ file_based_cache .set (sample_key , sample_data )
733+
734+ # Verify the expiry time that was set
735+ memory_result = file_based_cache .memory_cache .get (sample_key )
736+ expected_expiry = fixed_time + CACHE_EXPIRY_SECONDS
737+ assert memory_result .expiry_time == expected_expiry
738+
739+ # Clear memory cache to force file load on next get
740+ file_based_cache .memory_cache .clear ()
741+
742+ # Get data from file (should preserve the original expiry time from file)
743+ result = file_based_cache .get (sample_key )
744+
745+ assert result is not None
746+ assert (
747+ result .expiry_time == expected_expiry
748+ ) # Should preserve original expiry from file
749+ assert result .id == "test_connection"
750+
751+ # Verify it's also in memory cache with preserved expiry
752+ memory_result_after_load = file_based_cache .memory_cache .get (sample_key )
753+ assert memory_result_after_load is not None
754+ assert memory_result_after_load .expiry_time == expected_expiry
755+
756+
757+ def test_file_based_cache_deletes_expired_file_on_get (
758+ file_based_cache , encrypter_with_key , fixed_time
759+ ):
760+ """Test that FileBasedCache deletes expired files on get and returns cache miss."""
761+ sample_key = SecureCacheKey (["expired" , "file" ], "test_secret" )
762+ sample_data = ConnectionInfo (id = "test_connection" )
763+
764+ # Set data at an early time so it gets an early expiry
765+ early_time = fixed_time - 7200 # 2 hours before
766+ with patch ("time.time" , return_value = early_time ):
767+ file_based_cache .set (sample_key , sample_data )
768+
769+ # Verify the expiry time that was set (should be early_time + CACHE_EXPIRY_SECONDS)
770+ memory_result = file_based_cache .memory_cache .get (sample_key )
771+ expected_expiry = early_time + CACHE_EXPIRY_SECONDS
772+ assert memory_result .expiry_time == expected_expiry
773+
774+ # Verify file was created
775+ file_path = file_based_cache ._get_file_path (sample_key )
776+ assert os .path .exists (file_path )
777+
778+ # Clear memory cache to force file load
779+ file_based_cache .memory_cache .clear ()
780+
781+ # Now try to get at a time when the data should be expired
782+ # The data expires at early_time + CACHE_EXPIRY_SECONDS
783+ # Let's try to get it after that expiry time
784+ expired_check_time = early_time + CACHE_EXPIRY_SECONDS + 1
785+ with patch ("time.time" , return_value = expired_check_time ):
786+ result = file_based_cache .get (sample_key )
787+
788+ # Should return None due to expiry
789+ assert result is None
790+
791+ # File should be deleted
792+ assert not os .path .exists (file_path )
793+
794+ # Memory cache should not contain the data
795+ memory_result = file_based_cache .memory_cache .get (sample_key )
796+ assert memory_result is None
797+
798+
799+ def test_file_based_cache_expiry_edge_case_exactly_expired (
800+ file_based_cache , encrypter_with_key , fixed_time
801+ ):
802+ """Test behavior when data expires exactly at the current time."""
803+ sample_key = SecureCacheKey (["edge" , "case" ], "test_secret" )
804+ sample_data = ConnectionInfo (id = "test_connection" )
805+
806+ # Set data such that it will expire exactly at fixed_time
807+ set_time = fixed_time - CACHE_EXPIRY_SECONDS
808+ with patch ("time.time" , return_value = set_time ):
809+ file_based_cache .set (sample_key , sample_data )
810+
811+ # Verify the expiry time that was set
812+ memory_result = file_based_cache .memory_cache .get (sample_key )
813+ expected_expiry = set_time + CACHE_EXPIRY_SECONDS # This equals fixed_time
814+ assert memory_result .expiry_time == expected_expiry == fixed_time
815+
816+ file_path = file_based_cache ._get_file_path (sample_key )
817+ assert os .path .exists (file_path )
818+
819+ # Clear memory cache
820+ file_based_cache .memory_cache .clear ()
821+
822+ # Try to get exactly at expiry time (should be considered expired)
823+ with patch ("time.time" , return_value = fixed_time ):
824+ result = file_based_cache .get (sample_key )
825+
826+ # Should return None as data is expired (>= check in _is_expired)
827+ assert result is None
828+
829+ # File should be deleted
830+ assert not os .path .exists (file_path )
831+
832+
833+ def test_file_based_cache_non_expired_file_loads_correctly (
834+ file_based_cache , encrypter_with_key , fixed_time
835+ ):
836+ """Test that non-expired data from file loads correctly with preserved expiry."""
837+ sample_key = SecureCacheKey (["non" , "expired" ], "test_secret" )
838+
839+ sample_data = ConnectionInfo (id = "test_connection" , token = "test_token" )
840+
841+ # Set data at an earlier time so it's not expired yet
842+ set_time = fixed_time - 900 # 15 minutes before
843+ with patch ("time.time" , return_value = set_time ):
844+ file_based_cache .set (sample_key , sample_data )
845+
846+ # Verify expiry time
847+ memory_result = file_based_cache .memory_cache .get (sample_key )
848+ expected_expiry = set_time + CACHE_EXPIRY_SECONDS
849+ assert memory_result .expiry_time == expected_expiry
850+
851+ # Clear memory cache to force file load
852+ file_based_cache .memory_cache .clear ()
853+
854+ # Get data at fixed_time (data should not be expired since expected_expiry > fixed_time)
855+ with patch ("time.time" , return_value = fixed_time ):
856+ # Ensure the data is not expired
857+ assert expected_expiry > fixed_time , "Data should not be expired for this test"
858+
859+ result = file_based_cache .get (sample_key )
860+
861+ # Should successfully load data
862+ assert result is not None
863+ assert result .id == "test_connection"
864+ assert result .token == "test_token"
865+ assert result .expiry_time == expected_expiry # Preserved original expiry
866+
867+ # Verify file still exists (not deleted)
868+ file_path = file_based_cache ._get_file_path (sample_key )
869+ assert os .path .exists (file_path )
870+
871+ # Verify it's in memory cache with preserved expiry
872+ memory_result = file_based_cache .memory_cache .get (sample_key )
873+ assert memory_result is not None
874+ assert memory_result .expiry_time == expected_expiry
875+
876+
877+ def test_memory_cache_set_preserve_expiry_parameter (
878+ cache , sample_cache_key , fixed_time
879+ ):
880+ """Test UtilCache.set preserve_expiry parameter functionality."""
881+ # Create connection info with specific expiry time
882+ original_expiry = fixed_time + 1800
883+ sample_data = ConnectionInfo (id = "test_connection" , expiry_time = original_expiry )
884+
885+ with patch ("time.time" , return_value = fixed_time ):
886+ # Test preserve_expiry=True
887+ cache .set (sample_cache_key , sample_data , preserve_expiry = True )
888+
889+ result = cache .get (sample_cache_key )
890+ assert result is not None
891+ assert result .expiry_time == original_expiry # Should preserve original
892+
893+ cache .clear ()
894+
895+ # Test preserve_expiry=False (default behavior)
896+ cache .set (sample_cache_key , sample_data , preserve_expiry = False )
897+
898+ result = cache .get (sample_cache_key )
899+ assert result is not None
900+ expected_new_expiry = fixed_time + CACHE_EXPIRY_SECONDS
901+ assert result .expiry_time == expected_new_expiry # Should get new expiry
902+
903+ cache .clear ()
904+
905+ # Test default behavior (preserve_expiry not specified)
906+ cache .set (sample_cache_key , sample_data )
907+
908+ result = cache .get (sample_cache_key )
909+ assert result is not None
910+ assert result .expiry_time == expected_new_expiry # Should get new expiry
911+
912+
913+ def test_memory_cache_set_preserve_expiry_with_none_expiry (
914+ cache , sample_cache_key , fixed_time
915+ ):
916+ """Test UtilCache.set preserve_expiry when original expiry_time is None."""
917+ # Create connection info with None expiry time
918+ sample_data = ConnectionInfo (id = "test_connection" , expiry_time = None )
919+
920+ with patch ("time.time" , return_value = fixed_time ):
921+ # Even with preserve_expiry=True, None expiry should get new expiry
922+ cache .set (sample_cache_key , sample_data , preserve_expiry = True )
923+
924+ result = cache .get (sample_cache_key )
925+ assert result is not None
926+ expected_expiry = fixed_time + CACHE_EXPIRY_SECONDS
927+ assert (
928+ result .expiry_time == expected_expiry
929+ ) # Should get new expiry despite preserve=True
0 commit comments