from datetime import timedelta from backup.model.backups import Backup import pytest from backup.util import GlobalInfo from backup.ha import HaUpdater from backup.ha.haupdater import REASSURING_MESSAGE from .faketime import FakeTime from .helpers import HelperTestSource from dev.simulationserver import SimulationServer from backup.logger import getLast from backup.util import Estimator from dev.simulated_supervisor import SimulatedSupervisor, URL_MATCH_CORE_API from dev.request_interceptor import RequestInterceptor from backup.model import Coordinator from backup.config import Config, Setting STALE_ATTRIBUTES = { "friendly_name": "Backups Stale", "device_class": "problem" } @pytest.fixture def source(): return HelperTestSource("Source") @pytest.fixture def dest(): return HelperTestSource("Dest") @pytest.mark.asyncio async def test_init(updater: HaUpdater, global_info, supervisor: SimulatedSupervisor, server, time: FakeTime): await updater.update() assert not updater._stale() assert updater._state() == "waiting" verifyEntity(supervisor, "binary_sensor.backups_stale", "off", STALE_ATTRIBUTES) verifyEntity(supervisor, "sensor.backup_state", "waiting", { 'friendly_name': 'Backup State', 'last_backup': 'Never', 'next_backup': time.now().isoformat(), 'last_uploaded': 'Never', 'backups': [], 'backups_in_google_drive': 0, 'free_space_in_google_drive': "", 'backups_in_home_assistant': 0, 'size_in_google_drive': "0.0 B", 'size_in_home_assistant': '0.0 B' }) assert supervisor.getNotification() is None global_info.success() assert not updater._stale() assert updater._state() == "backed_up" @pytest.mark.asyncio async def test_init_failure(updater: HaUpdater, global_info: GlobalInfo, time: FakeTime, server, supervisor: SimulatedSupervisor): await updater.update() assert not updater._stale() assert updater._state() == "waiting" global_info.failed(Exception()) assert not updater._stale() assert updater._state() == "backed_up" assert supervisor.getNotification() is None time.advanceDay() assert updater._stale() assert updater._state() == "error" await updater.update() assert supervisor.getNotification() == { 'message': 'The add-on is having trouble making backups and needs attention. Please visit the add-on status page for details.', 'title': 'Home Assistant Google Drive Backup is Having Trouble', 'notification_id': 'backup_broken' } @pytest.mark.asyncio async def test_failure_backoff_502(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): interceptor.setError(URL_MATCH_CORE_API, 502) for x in range(9): await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] interceptor.clear() await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] @pytest.mark.asyncio async def test_failure_backoff_510(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): interceptor.setError(URL_MATCH_CORE_API, 502) for x in range(9): await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] interceptor.clear() await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] @pytest.mark.asyncio async def test_failure_backoff_other(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): interceptor.setError(URL_MATCH_CORE_API, 400) for x in range(9): await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] interceptor.clear() await updater.update() assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] @pytest.mark.asyncio async def test_update_backups(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor): await updater.update() assert not updater._stale() assert updater._state() == "waiting" verifyEntity(supervisor, "binary_sensor.backups_stale", "off", STALE_ATTRIBUTES) verifyEntity(supervisor, "sensor.backup_state", "waiting", { 'friendly_name': 'Backup State', 'last_backup': 'Never', 'next_backup': time.now().isoformat(), 'last_uploaded': 'Never', 'backups': [], 'backups_in_google_drive': 0, 'backups_in_home_assistant': 0, 'size_in_home_assistant': "0.0 B", 'size_in_google_drive': "0.0 B", 'free_space_in_google_drive': '' }) @pytest.mark.asyncio async def test_update_backups_no_next_backup(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) await updater.update() assert not updater._stale() assert updater._state() == "waiting" verifyEntity(supervisor, "binary_sensor.backups_stale", "off", STALE_ATTRIBUTES) verifyEntity(supervisor, "sensor.backup_state", "waiting", { 'friendly_name': 'Backup State', 'last_backup': 'Never', 'next_backup': None, 'last_uploaded': 'Never', 'backups': [], 'backups_in_google_drive': 0, 'backups_in_home_assistant': 0, 'size_in_home_assistant': "0.0 B", 'size_in_google_drive': "0.0 B", 'free_space_in_google_drive': '' }) @pytest.mark.asyncio async def test_update_backups_sync(updater: HaUpdater, server, time: FakeTime, backup: Backup, supervisor: SimulatedSupervisor, config: Config): await updater.update() assert not updater._stale() assert updater._state() == "backed_up" verifyEntity(supervisor, "binary_sensor.backups_stale", "off", STALE_ATTRIBUTES) date = '1985-12-06T05:00:00+00:00' verifyEntity(supervisor, "sensor.backup_state", "backed_up", { 'friendly_name': 'Backup State', 'last_backup': date, 'last_uploaded': date, 'next_backup': (backup.date() + timedelta(days=config.get(Setting.DAYS_BETWEEN_BACKUPS))).isoformat(), 'backups': [{ 'date': date, 'name': backup.name(), 'size': backup.sizeString(), 'state': backup.status(), 'slug': backup.slug() } ], 'backups_in_google_drive': 1, 'backups_in_home_assistant': 1, 'size_in_home_assistant': Estimator.asSizeString(backup.size()), 'size_in_google_drive': Estimator.asSizeString(backup.size()), 'free_space_in_google_drive': '5.0 GB' }) @pytest.mark.asyncio async def test_notification_link(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor): await updater.update() assert not updater._stale() assert updater._state() == "waiting" verifyEntity(supervisor, "binary_sensor.backups_stale", "off", STALE_ATTRIBUTES) verifyEntity(supervisor, "sensor.backup_state", "waiting", { 'friendly_name': 'Backup State', 'last_backup': 'Never', 'next_backup': time.now().isoformat(), 'last_uploaded': 'Never', 'backups': [], 'backups_in_google_drive': 0, 'backups_in_home_assistant': 0, 'size_in_home_assistant': "0.0 B", 'size_in_google_drive': "0.0 B", 'free_space_in_google_drive': '' }) assert supervisor.getNotification() is None global_info.failed(Exception()) global_info.url = "http://localhost/test" time.advanceDay() await updater.update() assert supervisor.getNotification() == { 'message': 'The add-on is having trouble making backups and needs attention. Please visit the add-on [status page](http://localhost/test) for details.', 'title': 'Home Assistant Google Drive Backup is Having Trouble', 'notification_id': 'backup_broken' } @pytest.mark.asyncio async def test_notification_clears(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor): await updater.update() assert not updater._stale() assert updater._state() == "waiting" assert supervisor.getNotification() is None global_info.failed(Exception()) time.advance(hours=8) await updater.update() assert supervisor.getNotification() is not None global_info.success() await updater.update() assert supervisor.getNotification() is None @pytest.mark.asyncio async def test_publish_for_failure(updater: HaUpdater, server, time: FakeTime, global_info: GlobalInfo, supervisor: SimulatedSupervisor): global_info.success() await updater.update() assert supervisor.getNotification() is None time.advance(hours=8) global_info.failed(Exception()) await updater.update() assert supervisor.getNotification() is not None time.advance(hours=8) global_info.failed(Exception()) await updater.update() assert supervisor.getNotification() is not None global_info.success() await updater.update() assert supervisor.getNotification() is None @pytest.mark.asyncio async def test_failure_logging(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): interceptor.setError(URL_MATCH_CORE_API, 501) assert getLast() is None await updater.update() assert getLast() is None time.advance(minutes=1) await updater.update() assert getLast() is None time.advance(minutes=5) await updater.update() assert getLast().msg == REASSURING_MESSAGE.format(501) last_log = getLast() time.advance(minutes=5) await updater.update() assert getLast() is not last_log assert getLast().msg == REASSURING_MESSAGE.format(501) last_log = getLast() interceptor.clear() await updater.update() assert getLast() is last_log @pytest.mark.asyncio async def test_publish_retries(updater: HaUpdater, server: SimulationServer, time: FakeTime, backup, drive, supervisor: SimulatedSupervisor): await updater.update() assert supervisor.getEntity("sensor.backup_state") is not None # Shoudlnt update after 59 minutes supervisor.clearEntities() time.advance(minutes=59) await updater.update() assert supervisor.getEntity("sensor.backup_state") is None # after that it should supervisor.clearEntities() time.advance(minutes=2) await updater.update() assert supervisor.getEntity("sensor.backup_state") is not None supervisor.clearEntities() await drive.delete(backup) await updater.update() assert supervisor.getEntity("sensor.backup_state") is not None @pytest.mark.asyncio async def test_ignored_backups(updater: HaUpdater, time: FakeTime, server: SimulationServer, backup: Backup, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config): config.override(Setting.IGNORE_OTHER_BACKUPS, True) time.advance(hours=1) await supervisor.createBackup({'name': "test_backup"}, date=time.now()) await coord.sync() await updater.update() state = supervisor.getAttributes("sensor.backup_state") assert state["backups_in_google_drive"] == 1 assert state["backups_in_home_assistant"] == 1 assert len(state["backups"]) == 1 assert state['last_backup'] == backup.date().isoformat() @pytest.mark.asyncio async def test_update_backups_old_names(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): config.override(Setting.CALL_BACKUP_SNAPSHOT, True) await updater.update() assert not updater._stale() assert updater._state() == "backed_up" verifyEntity(supervisor, "binary_sensor.snapshots_stale", "off", {"friendly_name": "Snapshots Stale", "device_class": "problem"}) date = '1985-12-06T05:00:00+00:00' verifyEntity(supervisor, "sensor.snapshot_backup", "backed_up", { 'friendly_name': 'Snapshot State', 'last_snapshot': date, 'snapshots': [{ 'date': date, 'name': backup.name(), 'size': backup.sizeString(), 'state': backup.status(), 'slug': backup.slug() } ], 'snapshots_in_google_drive': 1, 'snapshots_in_home_assistant': 1, 'snapshots_in_hassio': 1, 'size_in_home_assistant': Estimator.asSizeString(backup.size()), 'size_in_google_drive': Estimator.asSizeString(backup.size()) }) @pytest.mark.asyncio async def test_drive_free_space(updater: HaUpdater, time: FakeTime, server: SimulationServer, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config): await updater.update() state = supervisor.getAttributes("sensor.backup_state") assert state["free_space_in_google_drive"] == "" await coord.sync() await updater.update() state = supervisor.getAttributes("sensor.backup_state") assert state["free_space_in_google_drive"] == "5.0 GB" @pytest.mark.asyncio async def test_stale_backup_is_error(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "backed_up" time.advance(days=1) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "backed_up" time.advance(days=1) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "error" time.advance(days=1) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "error" @pytest.mark.asyncio async def test_stale_backup_ignores_pending(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator): config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "backed_up" time.advance(days=2) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "error" async with supervisor._backup_inner_lock: await coord.sync() assert coord.getBackup("pending") is not None await updater.update() assert supervisor.getEntity("sensor.backup_state") == "error" @pytest.mark.asyncio async def test_stale_backups_fine_for_no_creation(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator): config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "backed_up" # backups shouldn't become stale because the addon doesn't create them. time.advance(days=100) await updater.update() assert supervisor.getEntity("sensor.backup_state") == "backed_up" def verifyEntity(backend: SimulatedSupervisor, name, state, attributes): assert backend.getEntity(name) == state assert backend.getAttributes(name) == attributes