The default synchronization interval has been updated across the application to reflect a standard 30-minute delay (1800 seconds). This change updates both the source code configuration and the README documentation, ensuring that users rely on the accurate default value when no environment variable is provided. - Updated SYNC_INTERVAL_SECONDS default in src/main.py - Clarified single-run behavior in README.md
228 lines
6.6 KiB
Python
228 lines
6.6 KiB
Python
import importlib.util
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
MODULE_PATH = Path(__file__).parents[1] / "src" / "main.py"
|
|
SPEC = importlib.util.spec_from_file_location("album_sync", MODULE_PATH)
|
|
assert SPEC and SPEC.loader
|
|
album_sync = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(album_sync)
|
|
|
|
|
|
class AlbumNameTests(unittest.TestCase):
|
|
def test_root_directory_becomes_album_name(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
), patch.object(album_sync, "ALBUM_MODE", "root"):
|
|
self.assertEqual(
|
|
album_sync.get_album_name(
|
|
"/mnt/nextcloud-photos/Korea_2016_07_08/photo.jpg"
|
|
),
|
|
"Korea_2016_07_08",
|
|
)
|
|
|
|
def test_nested_files_stay_in_root_album(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
), patch.object(album_sync, "ALBUM_MODE", "root"):
|
|
self.assertEqual(
|
|
album_sync.get_album_name(
|
|
"/mnt/nextcloud-photos/Korea_2016_07_08/day-1/photo.jpg"
|
|
),
|
|
"Korea_2016_07_08",
|
|
)
|
|
|
|
def test_file_directly_in_external_root_is_skipped(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
):
|
|
self.assertIsNone(
|
|
album_sync.get_album_name(
|
|
"/mnt/nextcloud-photos/photo.jpg"
|
|
)
|
|
)
|
|
|
|
def test_similar_root_prefix_is_skipped(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
):
|
|
self.assertIsNone(
|
|
album_sync.get_album_name(
|
|
"/mnt/nextcloud-photos-old/Korea/photo.jpg"
|
|
)
|
|
)
|
|
|
|
def test_parent_traversal_cannot_escape_external_root(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
):
|
|
self.assertIsNone(
|
|
album_sync.get_album_name(
|
|
"/mnt/nextcloud-photos/../other/Korea/photo.jpg"
|
|
)
|
|
)
|
|
|
|
|
|
class SearchTests(unittest.TestCase):
|
|
def test_search_assets_follows_pagination(self) -> None:
|
|
responses = [
|
|
{
|
|
"assets": {
|
|
"items": [{"id": "one"}],
|
|
"nextPage": "2",
|
|
}
|
|
},
|
|
{
|
|
"assets": {
|
|
"items": [{"id": "two"}],
|
|
"nextPage": None,
|
|
}
|
|
},
|
|
]
|
|
|
|
with patch.object(
|
|
album_sync,
|
|
"api_request",
|
|
side_effect=responses,
|
|
) as request:
|
|
assets = album_sync.search_assets(libraryId="library-id")
|
|
|
|
self.assertEqual(assets, [{"id": "one"}, {"id": "two"}])
|
|
self.assertEqual(request.call_count, 2)
|
|
self.assertEqual(
|
|
request.call_args_list[0].kwargs["json"],
|
|
{
|
|
"libraryId": "library-id",
|
|
"page": 1,
|
|
"size": album_sync.PAGE_SIZE,
|
|
},
|
|
)
|
|
|
|
|
|
class SynchronizationTests(unittest.TestCase):
|
|
def test_main_creates_folder_album_with_all_assets(self) -> None:
|
|
assets = [
|
|
{
|
|
"id": "asset-one",
|
|
"originalPath": (
|
|
"/mnt/nextcloud-photos/"
|
|
"Korea_2016_07_08/photo-1.jpg"
|
|
),
|
|
},
|
|
{
|
|
"id": "asset-two",
|
|
"originalPath": (
|
|
"/mnt/nextcloud-photos/"
|
|
"Korea_2016_07_08/day-2/photo-2.jpg"
|
|
),
|
|
},
|
|
]
|
|
created_album = {
|
|
"id": "album-id",
|
|
"albumName": "Korea_2016_07_08",
|
|
}
|
|
|
|
with patch.object(
|
|
album_sync,
|
|
"EXTERNAL_ROOT",
|
|
"/mnt/nextcloud-photos",
|
|
), patch.object(
|
|
album_sync,
|
|
"ALBUM_MODE",
|
|
"root",
|
|
), patch.object(
|
|
album_sync,
|
|
"SYNC_INTERVAL_SECONDS",
|
|
0,
|
|
), patch.object(
|
|
album_sync,
|
|
"check_configuration",
|
|
), patch.object(
|
|
album_sync,
|
|
"get_albums",
|
|
return_value={},
|
|
), patch.object(
|
|
album_sync,
|
|
"get_external_assets",
|
|
return_value=assets,
|
|
), patch.object(
|
|
album_sync,
|
|
"create_album",
|
|
return_value=created_album,
|
|
) as create_album, patch.object(
|
|
album_sync,
|
|
"get_album_asset_ids",
|
|
return_value=set(),
|
|
), patch.object(
|
|
album_sync,
|
|
"add_assets_to_album",
|
|
) as add_assets:
|
|
result = album_sync.main()
|
|
|
|
self.assertEqual(result, 0)
|
|
create_album.assert_called_once_with("Korea_2016_07_08")
|
|
add_assets.assert_called_once_with(
|
|
"album-id",
|
|
["asset-one", "asset-two"],
|
|
)
|
|
|
|
def test_continuous_mode_waits_and_runs_again(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"SYNC_INTERVAL_SECONDS",
|
|
1800,
|
|
), patch.object(
|
|
album_sync,
|
|
"check_configuration",
|
|
), patch.object(
|
|
album_sync,
|
|
"synchronize",
|
|
side_effect=[None, KeyboardInterrupt],
|
|
) as synchronize, patch.object(
|
|
album_sync.time,
|
|
"sleep",
|
|
) as sleep:
|
|
with self.assertRaises(KeyboardInterrupt):
|
|
album_sync.main()
|
|
|
|
self.assertEqual(synchronize.call_count, 2)
|
|
sleep.assert_called_once_with(1800)
|
|
|
|
def test_continuous_mode_retries_after_sync_error(self) -> None:
|
|
with patch.object(
|
|
album_sync,
|
|
"SYNC_INTERVAL_SECONDS",
|
|
1800,
|
|
), patch.object(
|
|
album_sync,
|
|
"check_configuration",
|
|
), patch.object(
|
|
album_sync,
|
|
"synchronize",
|
|
side_effect=[RuntimeError("temporary"), KeyboardInterrupt],
|
|
) as synchronize, patch.object(
|
|
album_sync.time,
|
|
"sleep",
|
|
) as sleep:
|
|
with self.assertRaises(KeyboardInterrupt):
|
|
album_sync.main()
|
|
|
|
self.assertEqual(synchronize.call_count, 2)
|
|
sleep.assert_called_once_with(1800)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|