Update typing of BackupAgent.async_get_backup (#139923)

* Update typing of BackupAgent.async_get_backup

* Remove manual reset of frame helper
This commit is contained in:
Erik Montnemery 2025-03-06 17:25:34 +01:00 committed by GitHub
parent 88f18fdfdc
commit 6ba45a32c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 2 deletions

View File

@ -83,7 +83,7 @@ class BackupAgent(abc.ABC):
self, self,
backup_id: str, backup_id: str,
**kwargs: Any, **kwargs: Any,
) -> AgentBackup | None: ) -> AgentBackup:
"""Return a backup. """Return a backup.
Raises BackupNotFound if the backup does not exist. Raises BackupNotFound if the backup does not exist.

View File

@ -15,6 +15,7 @@ from multidict import istr
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import frame
from homeassistant.util import slugify from homeassistant.util import slugify
from . import util from . import util
@ -66,7 +67,12 @@ class DownloadBackupView(HomeAssistantView):
# Check for None to be backwards compatible with the old BackupAgent API, # Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10 # this can be removed in HA Core 2025.10
if backup is None: if not backup:
frame.report_usage(
"returns None from BackupAgent.async_get_backup",
breaks_in_ha_version="2025.10",
integration_domain=agent_id.partition(".")[0],
)
return Response(status=HTTPStatus.NOT_FOUND) return Response(status=HTTPStatus.NOT_FOUND)
headers = { headers = {

View File

@ -30,6 +30,7 @@ from homeassistant.backup_restore import (
from homeassistant.const import __version__ as HAVERSION from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
frame,
instance_id, instance_id,
integration_platform, integration_platform,
issue_registry as ir, issue_registry as ir,
@ -665,6 +666,11 @@ class BackupManager:
# Check for None to be backwards compatible with the old BackupAgent API, # Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10 # this can be removed in HA Core 2025.10
if not result: if not result:
frame.report_usage(
"returns None from BackupAgent.async_get_backup",
breaks_in_ha_version="2025.10",
integration_domain=agent_id.partition(".")[0],
)
continue continue
if backup is None: if backup is None:
if known_backup := self.known_backups.get(backup_id): if known_backup := self.known_backups.get(backup_id):
@ -1280,6 +1286,11 @@ class BackupManager:
# Check for None to be backwards compatible with the old BackupAgent API, # Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10 # this can be removed in HA Core 2025.10
if not backup: if not backup:
frame.report_usage(
"returns None from BackupAgent.async_get_backup",
breaks_in_ha_version="2025.10",
integration_domain=agent_id.partition(".")[0],
)
raise BackupManagerError( raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}" f"Backup {backup_id} not found in agent {agent_id}"
) )
@ -1376,6 +1387,11 @@ class BackupManager:
# Check for None to be backwards compatible with the old BackupAgent API, # Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10 # this can be removed in HA Core 2025.10
if not backup: if not backup:
frame.report_usage(
"returns None from BackupAgent.async_get_backup",
breaks_in_ha_version="2025.10",
integration_domain=agent_id.partition(".")[0],
)
raise BackupManagerError( raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}" f"Backup {backup_id} not found in agent {agent_id}"
) )

View File

@ -229,6 +229,17 @@
'type': 'result', 'type': 'result',
}) })
# --- # ---
# name: test_can_decrypt_on_download_get_backup_returns_none
dict({
'error': dict({
'code': 'home_assistant_error',
'message': 'Backup abc123 not found in agent test.remote',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_can_decrypt_on_download_with_agent_error[BackupAgentError] # name: test_can_decrypt_on_download_with_agent_error[BackupAgentError]
dict({ dict({
'error': dict({ 'error': dict({
@ -4930,6 +4941,18 @@
'type': 'result', 'type': 'result',
}) })
# --- # ---
# name: test_details_get_backup_returns_none
dict({
'id': 1,
'result': dict({
'agent_errors': dict({
}),
'backup': None,
}),
'success': True,
'type': 'result',
})
# ---
# name: test_details_with_errors[BackupAgentUnreachableError] # name: test_details_with_errors[BackupAgentUnreachableError]
dict({ dict({
'id': 1, 'id': 1,
@ -5728,6 +5751,17 @@
# name: test_restore_remote_agent[remote_agents1-backups1].1 # name: test_restore_remote_agent[remote_agents1-backups1].1
1 1
# --- # ---
# name: test_restore_remote_agent_get_backup_returns_none
dict({
'error': dict({
'code': 'home_assistant_error',
'message': 'Backup abc123 not found in agent test.remote',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_restore_wrong_password # name: test_restore_wrong_password
dict({ dict({
'error': dict({ 'error': dict({

View File

@ -234,6 +234,26 @@ async def test_downloading_backup_not_found(
assert resp.status == 404 assert resp.status == 404
async def test_downloading_backup_not_found_get_backup_returns_none(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test downloading a backup file that does not exist."""
mock_agents = await setup_backup_integration(hass, remote_agents=["test.test"])
mock_agents["test.test"].async_get_backup.return_value = None
mock_agents["test.test"].async_get_backup.side_effect = None
client = await hass_client()
resp = await client.get("/api/backup/download/abc123?agent_id=test.test")
assert resp.status == 404
assert (
"Detected that integration 'test' returns None from BackupAgent.async_get_backup."
in caplog.text
)
async def test_downloading_as_non_admin( async def test_downloading_as_non_admin(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,

View File

@ -234,6 +234,31 @@ async def test_details_with_errors(
assert await client.receive_json() == snapshot assert await client.receive_json() == snapshot
async def test_details_get_backup_returns_none(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info when the agent returns None from get_backup."""
mock_agents = await setup_backup_integration(hass, remote_agents=["test.remote"])
mock_agents["test.remote"].async_get_backup.return_value = None
mock_agents["test.remote"].async_get_backup.side_effect = None
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch("pathlib.Path.exists", return_value=True):
await client.send_json_auto_id(
{"type": "backup/details", "backup_id": "abc123"}
)
assert await client.receive_json() == snapshot
assert (
"Detected that integration 'test' returns None from BackupAgent.async_get_backup."
in caplog.text
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("remote_agents", "backups"), ("remote_agents", "backups"),
[ [
@ -724,6 +749,36 @@ async def test_restore_remote_agent(
assert len(restart_calls) == snapshot assert len(restart_calls) == snapshot
async def test_restore_remote_agent_get_backup_returns_none(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
"""Test calling the restore command when the agent returns None from get_backup."""
mock_agents = await setup_backup_integration(hass, remote_agents=["test.remote"])
mock_agents["test.remote"].async_get_backup.return_value = None
mock_agents["test.remote"].async_get_backup.side_effect = None
restart_calls = async_mock_service(hass, "homeassistant", "restart")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json_auto_id(
{
"type": "backup/restore",
"backup_id": "abc123",
"agent_id": "test.remote",
}
)
assert await client.receive_json() == snapshot
assert len(restart_calls) == 0
assert (
"Detected that integration 'test' returns None from BackupAgent.async_get_backup."
in caplog.text
)
async def test_restore_wrong_password( async def test_restore_wrong_password(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
@ -3543,3 +3598,32 @@ async def test_can_decrypt_on_download_with_agent_error(
} }
) )
assert await client.receive_json() == snapshot assert await client.receive_json() == snapshot
@pytest.mark.usefixtures("mock_backups")
async def test_can_decrypt_on_download_get_backup_returns_none(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
) -> None:
"""Test can decrypt on download when the agent returns None from get_backup."""
mock_agents = await setup_backup_integration(hass, remote_agents=["test.remote"])
mock_agents["test.remote"].async_get_backup.return_value = None
mock_agents["test.remote"].async_get_backup.side_effect = None
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "backup/can_decrypt_on_download",
"backup_id": TEST_BACKUP_ABC123.backup_id,
"agent_id": "test.remote",
"password": "hunter2",
}
)
assert await client.receive_json() == snapshot
assert (
"Detected that integration 'test' returns None from BackupAgent.async_get_backup."
in caplog.text
)