feat(storage): full object checksum: implement rolling checksum and v… · googleapis/google-cloud-python@2361ba6
@@ -45,6 +45,48 @@ def test_initialization(self):
4545self.assertEqual(state.bytes_written, 0)
4646self.assertEqual(state.next_expected_offset, initial_offset)
4747self.assertFalse(state.is_complete)
48+self.assertFalse(state.is_full_object_read)
49+self.assertIsNone(state.rolling_checksum)
50+51+def test_initialization_with_full_object_read(self):
52+"""Test that _DownloadState initializes correctly when is_full_object_read is True."""
53+initial_offset = 10
54+initial_length = 100
55+user_buffer = io.BytesIO()
56+state_full = _DownloadState(
57+initial_offset, initial_length, user_buffer, is_full_object_read=True
58+ )
59+60+self.assertEqual(state_full.initial_offset, initial_offset)
61+self.assertEqual(state_full.initial_length, initial_length)
62+self.assertEqual(state_full.user_buffer, user_buffer)
63+self.assertEqual(state_full.bytes_written, 0)
64+self.assertEqual(state_full.next_expected_offset, initial_offset)
65+self.assertFalse(state_full.is_complete)
66+self.assertTrue(state_full.is_full_object_read)
67+self.assertIsNotNone(state_full.rolling_checksum)
68+69+def test_initialization_with_full_object_read_and_checksum_disabled(self):
70+"""Test that _DownloadState does not initialize rolling_checksum when enable_checksum is False."""
71+initial_offset = 10
72+initial_length = 100
73+user_buffer = io.BytesIO()
74+state_full = _DownloadState(
75+initial_offset,
76+initial_length,
77+user_buffer,
78+is_full_object_read=True,
79+enable_checksum=False,
80+ )
81+82+self.assertEqual(state_full.initial_offset, initial_offset)
83+self.assertEqual(state_full.initial_length, initial_length)
84+self.assertEqual(state_full.user_buffer, user_buffer)
85+self.assertEqual(state_full.bytes_written, 0)
86+self.assertEqual(state_full.next_expected_offset, initial_offset)
87+self.assertFalse(state_full.is_complete)
88+self.assertTrue(state_full.is_full_object_read)
89+self.assertIsNone(state_full.rolling_checksum)
489049915092class TestReadResumptionStrategy(unittest.TestCase):
@@ -53,12 +95,24 @@ def setUp(self):
53955496self.state = {"download_states": {}, "read_handle": None, "routing_token": None}
559756-def _add_download(self, read_id, offset=0, length=100, buffer=None):
98+def _add_download(
99+self,
100+read_id,
101+offset=0,
102+length=100,
103+buffer=None,
104+is_full_object_read=False,
105+enable_checksum=True,
106+ ):
57107"""Helper to inject a download state into the correct nested location."""
58108if buffer is None:
59109buffer = io.BytesIO()
60110state = _DownloadState(
61-initial_offset=offset, initial_length=length, user_buffer=buffer
111+initial_offset=offset,
112+initial_length=length,
113+user_buffer=buffer,
114+is_full_object_read=is_full_object_read,
115+enable_checksum=enable_checksum,
62116 )
63117self.state["download_states"][read_id] = state
64118return state
@@ -358,3 +412,61 @@ async def run():
358412359413# Token should remain unchanged
360414self.assertEqual(self.state["routing_token"], "existing-token")
415+416+def test_update_state_full_object_checksum_success(self):
417+"""Test that full object checksum verification succeeds on range_end."""
418+read_state = self._add_download(
419+_READ_ID, offset=0, length=9, is_full_object_read=True
420+ )
421+self.state["enable_checksum"] = True
422+self.state["full_obj_server_crc32c"] = google_crc32c.value(b"testdata1")
423+424+resp1 = self._create_response(b"test", _READ_ID, offset=0)
425+self.strategy.update_state_from_response(resp1, self.state)
426+427+resp2 = self._create_response(b"data1", _READ_ID, offset=4, range_end=True)
428+self.strategy.update_state_from_response(resp2, self.state)
429+430+self.assertTrue(read_state.is_complete)
431+self.assertEqual(read_state.bytes_written, 9)
432+433+def test_update_state_full_object_checksum_failure(self):
434+"""Test that full object checksum verification raises DataCorruption on mismatch at range_end."""
435+self._add_download(_READ_ID, offset=0, length=9, is_full_object_read=True)
436+self.state["enable_checksum"] = True
437+self.state["full_obj_server_crc32c"] = 111111 # Wrong server checksum!
438+439+resp1 = self._create_response(b"test", _READ_ID, offset=0)
440+self.strategy.update_state_from_response(resp1, self.state)
441+442+resp2 = self._create_response(b"data1", _READ_ID, offset=4, range_end=True)
443+with self.assertRaisesRegex(DataCorruption, "Full object checksum mismatch"):
444+self.strategy.update_state_from_response(resp2, self.state)
445+446+def test_update_state_checksum_mismatch_ignored_when_disabled(self):
447+"""Test that a CRC32C mismatch is ignored when enable_checksum is False."""
448+self._add_download(_READ_ID)
449+self.state["enable_checksum"] = False
450+response = self._create_response(b"data", _READ_ID, offset=0, crc=999999)
451+452+# Should NOT raise DataCorruption!
453+self.strategy.update_state_from_response(response, self.state)
454+455+def test_update_state_full_object_checksum_mismatch_ignored_when_disabled(self):
456+"""Test that a full-object CRC32C mismatch is ignored when enable_checksum is False."""
457+self._add_download(
458+_READ_ID,
459+offset=0,
460+length=9,
461+is_full_object_read=True,
462+enable_checksum=False,
463+ )
464+self.state["enable_checksum"] = False
465+self.state["full_obj_server_crc32c"] = 111111 # Wrong server checksum!
466+467+resp1 = self._create_response(b"test", _READ_ID, offset=0)
468+self.strategy.update_state_from_response(resp1, self.state)
469+470+resp2 = self._create_response(b"data1", _READ_ID, offset=4, range_end=True)
471+# Should NOT raise DataCorruption!
472+self.strategy.update_state_from_response(resp2, self.state)