Skip to content

Commit 0996ad4

Browse files
Add pagination support for IDrive e2 (home-assistant#162960)
1 parent e8885de commit 0996ad4

File tree

3 files changed

+135
-35
lines changed

3 files changed

+135
-35
lines changed

homeassistant/components/idrive_e2/backup.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,14 @@ async def _list_backups(self) -> dict[str, AgentBackup]:
329329
return self._backup_cache
330330

331331
backups = {}
332-
response = await cast(Any, self._client).list_objects_v2(Bucket=self._bucket)
333-
334-
# Filter for metadata files only
335-
metadata_files = [
336-
obj
337-
for obj in response.get("Contents", [])
338-
if obj["Key"].endswith(".metadata.json")
339-
]
332+
paginator = self._client.get_paginator("list_objects_v2")
333+
metadata_files: list[dict[str, Any]] = []
334+
async for page in paginator.paginate(Bucket=self._bucket):
335+
metadata_files.extend(
336+
obj
337+
for obj in page.get("Contents", [])
338+
if obj["Key"].endswith(".metadata.json")
339+
)
340340

341341
for metadata_file in metadata_files:
342342
try:

tests/components/idrive_e2/conftest.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from collections.abc import AsyncIterator, Generator
66
import json
7-
from unittest.mock import AsyncMock, patch
7+
from unittest.mock import AsyncMock, MagicMock, patch
88

99
import pytest
1010

@@ -53,9 +53,11 @@ def mock_client(agent_backup: AgentBackup) -> Generator[AsyncMock]:
5353
client = create_client.return_value
5454

5555
tar_file, metadata_file = suggested_filenames(agent_backup)
56-
client.list_objects_v2.return_value = {
57-
"Contents": [{"Key": tar_file}, {"Key": metadata_file}]
58-
}
56+
# Mock the paginator for list_objects_v2
57+
client.get_paginator = MagicMock()
58+
client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
59+
{"Contents": [{"Key": tar_file}, {"Key": metadata_file}]}
60+
]
5961
client.create_multipart_upload.return_value = {"UploadId": "upload_id"}
6062
client.upload_part.return_value = {"ETag": "etag"}
6163
client.list_buckets.return_value = {

tests/components/idrive_e2/test_backup.py

Lines changed: 121 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ async def test_agents_get_backup_does_not_throw_on_not_found(
179179
mock_client: MagicMock,
180180
) -> None:
181181
"""Test agent get backup does not throw on a backup not found."""
182-
mock_client.list_objects_v2.return_value = {"Contents": []}
182+
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
183+
{"Contents": []}
184+
]
183185

184186
client = await hass_ws_client(hass)
185187
await client.send_json_auto_id({"type": "backup/details", "backup_id": "random"})
@@ -202,18 +204,20 @@ async def test_agents_list_backups_with_corrupted_metadata(
202204
agent = IDriveE2BackupAgent(hass, mock_config_entry)
203205

204206
# Set up mock responses for both valid and corrupted metadata files
205-
mock_client.list_objects_v2.return_value = {
206-
"Contents": [
207-
{
208-
"Key": "valid_backup.metadata.json",
209-
"LastModified": "2023-01-01T00:00:00+00:00",
210-
},
211-
{
212-
"Key": "corrupted_backup.metadata.json",
213-
"LastModified": "2023-01-01T00:00:00+00:00",
214-
},
215-
]
216-
}
207+
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
208+
{
209+
"Contents": [
210+
{
211+
"Key": "valid_backup.metadata.json",
212+
"LastModified": "2023-01-01T00:00:00+00:00",
213+
},
214+
{
215+
"Key": "corrupted_backup.metadata.json",
216+
"LastModified": "2023-01-01T00:00:00+00:00",
217+
},
218+
]
219+
}
220+
]
217221

218222
# Mock responses for get_object calls
219223
valid_metadata = json.dumps(agent_backup.as_dict())
@@ -270,7 +274,9 @@ async def test_agents_delete_not_throwing_on_not_found(
270274
mock_client: MagicMock,
271275
) -> None:
272276
"""Test agent delete backup does not throw on a backup not found."""
273-
mock_client.list_objects_v2.return_value = {"Contents": []}
277+
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
278+
{"Contents": []}
279+
]
274280

275281
client = await hass_ws_client(hass)
276282

@@ -284,7 +290,7 @@ async def test_agents_delete_not_throwing_on_not_found(
284290

285291
assert response["success"]
286292
assert response["result"] == {"agent_errors": {}}
287-
assert mock_client.delete_object.call_count == 0
293+
assert mock_client.delete_objects.call_count == 0
288294

289295

290296
async def test_agents_upload(
@@ -490,28 +496,35 @@ async def test_cache_expiration(
490496
metadata_content = json.dumps(agent_backup.as_dict())
491497
mock_body = AsyncMock()
492498
mock_body.read.return_value = metadata_content.encode()
493-
mock_client.list_objects_v2.return_value = {
494-
"Contents": [
495-
{"Key": "test.metadata.json", "LastModified": "2023-01-01T00:00:00+00:00"}
496-
]
497-
}
499+
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
500+
{
501+
"Contents": [
502+
{
503+
"Key": "test.metadata.json",
504+
"LastModified": "2023-01-01T00:00:00+00:00",
505+
}
506+
]
507+
}
508+
]
509+
510+
mock_client.get_object.return_value = {"Body": mock_body}
498511

499512
# First call should query IDrive e2
500513
await agent.async_list_backups()
501-
assert mock_client.list_objects_v2.call_count == 1
514+
assert mock_client.get_paginator.call_count == 1
502515
assert mock_client.get_object.call_count == 1
503516

504517
# Second call should use cache
505518
await agent.async_list_backups()
506-
assert mock_client.list_objects_v2.call_count == 1
519+
assert mock_client.get_paginator.call_count == 1
507520
assert mock_client.get_object.call_count == 1
508521

509522
# Set cache to expire
510523
agent._cache_expiration = time() - 1
511524

512525
# Third call should query IDrive e2 again
513526
await agent.async_list_backups()
514-
assert mock_client.list_objects_v2.call_count == 2
527+
assert mock_client.get_paginator.call_count == 2
515528
assert mock_client.get_object.call_count == 2
516529

517530

@@ -526,3 +539,88 @@ async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None:
526539
remove_listener()
527540

528541
assert DATA_BACKUP_AGENT_LISTENERS not in hass.data
542+
543+
544+
async def test_list_backups_with_pagination(
545+
hass: HomeAssistant,
546+
mock_config_entry: MockConfigEntry,
547+
) -> None:
548+
"""Test listing backups when paginating through multiple pages."""
549+
# Create agent
550+
agent = IDriveE2BackupAgent(hass, mock_config_entry)
551+
552+
# Create two different backups
553+
backup1 = AgentBackup(
554+
backup_id="backup1",
555+
date="2023-01-01T00:00:00+00:00",
556+
addons=[],
557+
database_included=False,
558+
extra_metadata={},
559+
folders=[],
560+
homeassistant_included=False,
561+
homeassistant_version=None,
562+
name="Backup 1",
563+
protected=False,
564+
size=0,
565+
)
566+
backup2 = AgentBackup(
567+
backup_id="backup2",
568+
date="2023-01-02T00:00:00+00:00",
569+
addons=[],
570+
database_included=False,
571+
extra_metadata={},
572+
folders=[],
573+
homeassistant_included=False,
574+
homeassistant_version=None,
575+
name="Backup 2",
576+
protected=False,
577+
size=0,
578+
)
579+
580+
# Setup two pages of results
581+
page1 = {
582+
"Contents": [
583+
{
584+
"Key": "backup1.metadata.json",
585+
"LastModified": "2023-01-01T00:00:00+00:00",
586+
},
587+
{"Key": "backup1.tar", "LastModified": "2023-01-01T00:00:00+00:00"},
588+
]
589+
}
590+
page2 = {
591+
"Contents": [
592+
{
593+
"Key": "backup2.metadata.json",
594+
"LastModified": "2023-01-02T00:00:00+00:00",
595+
},
596+
{"Key": "backup2.tar", "LastModified": "2023-01-02T00:00:00+00:00"},
597+
]
598+
}
599+
600+
# Setup mock client
601+
mock_client = mock_config_entry.runtime_data
602+
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
603+
page1,
604+
page2,
605+
]
606+
607+
# Mock get_object responses based on the key
608+
async def mock_get_object(**kwargs):
609+
"""Mock get_object with different responses based on the key."""
610+
key = kwargs.get("Key", "")
611+
if "backup1" in key:
612+
mock_body = AsyncMock()
613+
mock_body.read.return_value = json.dumps(backup1.as_dict()).encode()
614+
return {"Body": mock_body}
615+
# backup2
616+
mock_body = AsyncMock()
617+
mock_body.read.return_value = json.dumps(backup2.as_dict()).encode()
618+
return {"Body": mock_body}
619+
620+
mock_client.get_object.side_effect = mock_get_object
621+
622+
# List backups and verify we got both
623+
backups = await agent.async_list_backups()
624+
assert len(backups) == 2
625+
backup_ids = {backup.backup_id for backup in backups}
626+
assert backup_ids == {"backup1", "backup2"}

0 commit comments

Comments
 (0)