Sunil Mohan Adapa b609abe7e5
*: Use the App's state management API
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2021-12-04 16:37:40 -05:00

479 lines
14 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Test scheduling of backups.
"""
import json
from datetime import datetime, timedelta
from unittest.mock import MagicMock, call, patch
import pytest
import plinth.modules.backups.repository # noqa, pylint: disable=unused-import
from plinth.app import App
from ..components import BackupRestore
from ..schedule import Schedule
class AppTest(App):
"""Sample App for testing."""
app_id = 'test-app'
def _get_backup_component(name):
"""Return a BackupRestore component."""
return BackupRestore(name)
def _get_test_app(name):
"""Return an App."""
app = AppTest()
app.app_id = name
app._all_apps[name] = app
app.add(BackupRestore(name + '-component'))
return app
def test_init_default_values():
"""Test initialization of schedule with default values."""
schedule = Schedule('test-uuid')
assert schedule.repository_uuid == 'test-uuid'
assert not schedule.enabled
assert schedule.daily_to_keep == 5
assert schedule.weekly_to_keep == 3
assert schedule.monthly_to_keep == 3
assert schedule.run_at_hour == 2
assert schedule.unselected_apps == []
def test_init():
"""Test initialization with explicit values."""
schedule = Schedule('test-uuid', enabled=True, daily_to_keep=1,
weekly_to_keep=2, monthly_to_keep=5, run_at_hour=0,
unselected_apps=['test-app1', 'test-app2'])
assert schedule.repository_uuid == 'test-uuid'
assert schedule.enabled
assert schedule.daily_to_keep == 1
assert schedule.weekly_to_keep == 2
assert schedule.monthly_to_keep == 5
assert schedule.run_at_hour == 0
assert schedule.unselected_apps == ['test-app1', 'test-app2']
def test_get_storage_format():
"""Test that storage format is properly returned."""
schedule = Schedule('test-uuid', enabled=True, daily_to_keep=1,
weekly_to_keep=2, monthly_to_keep=5, run_at_hour=23,
unselected_apps=['test-app1', 'test-app2'])
assert schedule.get_storage_format() == {
'enabled': True,
'daily_to_keep': 1,
'weekly_to_keep': 2,
'monthly_to_keep': 5,
'run_at_hour': 23,
'unselected_apps': ['test-app1', 'test-app2'],
}
def _get_archives_from_test_data(data):
"""Return a list of archives from test data."""
archives = []
for index, item in enumerate(data):
archive_time = item['time']
if isinstance(archive_time, str):
archive_time = datetime.strptime(archive_time,
'%Y-%m-%d %H:%M:%S+0000')
archive = {
'comment':
json.dumps({
'type': 'scheduled',
'periods': item['periods']
}),
'start':
archive_time.strftime('%Y-%m-%dT%H:%M:%S.%f'),
'name':
f'archive-{index}'
}
archives.append(archive)
return archives
# - First item is the arguments to send construct Schedule()
# - Second item is the list of previous backups in the system.
# - Third item is the return value of datetime.datetime.now().
# - Fourth item is the list of periods for which backups must be triggered.
# - Fifth item is the list of expected archives to be deleted after backup.
cases = [
# Schedule is disabled
[
[False, 10, 10, 10, 0],
[],
datetime(2021, 1, 1),
[],
[],
],
# No past backups
[
[True, 10, 10, 10, 0],
[],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# Daily backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Weekly backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Monthly backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['monthly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Backup taken not so recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# Too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(days=1, seconds=3601)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(days=1, seconds=3600)
}],
datetime(2021, 1, 1),
['weekly', 'monthly'],
[],
],
# Too long since a weekly backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(days=7, seconds=3601)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(days=7, seconds=3600)
}],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Too long since a monthly backup, not scheduled time, year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 12, 1)
}],
datetime(2021, 1, 1, 1, 0, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a monthly backup, not scheduled time, year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 12, 1)
}],
datetime(2021, 1, 1, 1),
['daily', 'weekly'],
[],
],
# Too long since a monthly backup, not scheduled time, no year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 11, 1)
}],
datetime(2020, 12, 1, 1, 0, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a monthly backup, not scheduled time, no year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 11, 1)
}],
datetime(2020, 12, 1, 1),
['daily', 'weekly'],
[],
],
# Time for daily backup
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2),
['daily'],
[],
],
# Time for daily backup, different scheduled time
[
[True, 10, 10, 10, 11],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2, 11) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2, 11),
['daily'],
[],
],
# Time for daily/weekly backup, 2021-01-03 is a Sunday
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 3) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 3),
['daily', 'weekly'],
[],
],
# Time for daily/monthly backup
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Daily backups disabled by setting the no. of backups to keep to 0
[
[True, 0, 10, 10, 0],
[],
datetime(2021, 1, 1),
['weekly', 'monthly'],
[],
],
# Weekly backups disabled by setting the no. of backups to keep to 0
[
[True, 10, 0, 10, 0],
[],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Monthly backups disabled by setting the no. of backups to keep to 0
[
[True, 10, 10, 0, 0],
[],
datetime(2021, 1, 1),
['daily', 'weekly'],
[],
],
# Not scheduled, not too long since last, no backup necessary
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2, 1),
[],
[],
],
# Cleanup daily backups
[
[True, 2, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'weekly', 'monthly'],
['archive-2', 'archive-3'],
],
# Cleanup weekly backups
[
[True, 10, 2, 10, 0],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'monthly'],
['archive-2', 'archive-3'],
],
# Cleanup monthly backups
[
[True, 10, 10, 2, 0],
[{
'periods': ['monthly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'weekly'],
['archive-2', 'archive-3'],
],
# Cleanup daily backups, but keep due to them being weekly/monthly too
[
[True, 2, 1, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 6, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 5, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'weekly'],
'time': datetime(2021, 1, 4, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'weekly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'monthly'],
'time': datetime(2021, 1, 2, 0) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 7, 1),
['daily'],
['archive-3', 'archive-5'],
],
]
@pytest.mark.parametrize(
'schedule_params,archives_data,test_now,run_periods,cleanups', cases)
@patch('plinth.app.App.get_setup_state')
@patch('plinth.modules.backups.repository.get_instance')
def test_run_schedule(get_instance, get_setup_state, schedule_params,
archives_data, test_now, run_periods, cleanups):
"""Test that backups are run at expected time."""
get_setup_state.return_value = App.SetupState.UP_TO_DATE
repository = MagicMock()
repository.list_archives.side_effect = \
lambda: _get_archives_from_test_data(archives_data)
get_instance.return_value = repository
with patch('plinth.modules.backups.schedule.datetime') as mock_datetime, \
patch('plinth.app.App.list') as app_list:
app_list.return_value = [
_get_test_app('test-app1'),
_get_test_app('test-app2'),
_get_test_app('test-app3')
]
mock_datetime.now.return_value = test_now
mock_datetime.strptime = datetime.strptime
mock_datetime.min = datetime.min
mock_datetime.side_effect = lambda *args, **kwargs: datetime(
*args, **kwargs)
schedule = Schedule('test_uuid', schedule_params[0],
schedule_params[1], schedule_params[2],
schedule_params[3], schedule_params[4],
['test-app2'])
schedule.run_schedule()
if not run_periods:
repository.create_archive.assert_not_called()
else:
run_periods.sort()
name = 'scheduled: {periods}: {datetime}'.format(
periods=', '.join(run_periods),
datetime=mock_datetime.now().strftime('%Y-%m-%d:%H:%M'))
app_ids = ['test-app1', 'test-app3']
archive_comment = json.dumps({
'type': 'scheduled',
'periods': run_periods
}).replace('{', '{{').replace('}', '}}')
repository.create_archive.assert_has_calls(
[call(name, app_ids, archive_comment=archive_comment)])
if not cleanups:
repository.delete_archive.assert_not_called()
else:
calls = [call(name) for name in cleanups]
repository.delete_archive.assert_has_calls(calls)