Some checks failed
Validate / Hassfest (push) Has been cancelled
Extract HA-independent logic from the integration into packages/core/ as a standalone Python library (immich-watcher-core). This is the first phase of restructuring the project to support a standalone web app alongside the existing HAOS integration. Core library modules: - models: SharedLinkInfo, AssetInfo, AlbumData, AlbumChange dataclasses - immich_client: Async Immich API client (aiohttp, session-injected) - change_detector: Pure function for album change detection - asset_utils: Filtering, sorting, URL building utilities - telegram/client: Full Telegram Bot API (text, photo, video, media groups) - telegram/cache: File ID cache with pluggable storage backend - telegram/media: Media size checks, URL extraction, group splitting - notifications/queue: Persistent notification queue - storage: StorageBackend protocol + JSON file implementation All modules have zero Home Assistant imports. 50 unit tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
186 lines
5.7 KiB
Python
186 lines
5.7 KiB
Python
"""Tests for data models."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from immich_watcher_core.models import (
|
|
AlbumChange,
|
|
AlbumData,
|
|
AssetInfo,
|
|
SharedLinkInfo,
|
|
)
|
|
|
|
|
|
class TestSharedLinkInfo:
|
|
def test_from_api_response_basic(self):
|
|
data = {"id": "link-1", "key": "abc123"}
|
|
link = SharedLinkInfo.from_api_response(data)
|
|
assert link.id == "link-1"
|
|
assert link.key == "abc123"
|
|
assert not link.has_password
|
|
assert link.is_accessible
|
|
|
|
def test_from_api_response_with_password(self):
|
|
data = {"id": "link-1", "key": "abc123", "password": "secret"}
|
|
link = SharedLinkInfo.from_api_response(data)
|
|
assert link.has_password
|
|
assert link.password == "secret"
|
|
assert not link.is_accessible
|
|
|
|
def test_from_api_response_with_expiry(self):
|
|
data = {
|
|
"id": "link-1",
|
|
"key": "abc123",
|
|
"expiresAt": "2099-12-31T23:59:59Z",
|
|
}
|
|
link = SharedLinkInfo.from_api_response(data)
|
|
assert link.expires_at is not None
|
|
assert not link.is_expired
|
|
|
|
def test_expired_link(self):
|
|
link = SharedLinkInfo(
|
|
id="link-1",
|
|
key="abc123",
|
|
expires_at=datetime(2020, 1, 1, tzinfo=timezone.utc),
|
|
)
|
|
assert link.is_expired
|
|
assert not link.is_accessible
|
|
|
|
|
|
class TestAssetInfo:
|
|
def test_from_api_response_image(self):
|
|
data = {
|
|
"id": "asset-1",
|
|
"type": "IMAGE",
|
|
"originalFileName": "photo.jpg",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
"thumbhash": "abc123",
|
|
}
|
|
asset = AssetInfo.from_api_response(data, {"user-1": "Alice"})
|
|
assert asset.id == "asset-1"
|
|
assert asset.type == "IMAGE"
|
|
assert asset.filename == "photo.jpg"
|
|
assert asset.owner_name == "Alice"
|
|
assert asset.is_processed
|
|
|
|
def test_from_api_response_with_exif(self):
|
|
data = {
|
|
"id": "asset-2",
|
|
"type": "IMAGE",
|
|
"originalFileName": "photo.jpg",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
"isFavorite": True,
|
|
"thumbhash": "xyz",
|
|
"exifInfo": {
|
|
"rating": 5,
|
|
"latitude": 48.8566,
|
|
"longitude": 2.3522,
|
|
"city": "Paris",
|
|
"state": "Île-de-France",
|
|
"country": "France",
|
|
"description": "Eiffel Tower",
|
|
},
|
|
}
|
|
asset = AssetInfo.from_api_response(data)
|
|
assert asset.is_favorite
|
|
assert asset.rating == 5
|
|
assert asset.latitude == 48.8566
|
|
assert asset.city == "Paris"
|
|
assert asset.description == "Eiffel Tower"
|
|
|
|
def test_unprocessed_asset(self):
|
|
data = {
|
|
"id": "asset-3",
|
|
"type": "VIDEO",
|
|
"originalFileName": "video.mp4",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
# No thumbhash = not processed
|
|
}
|
|
asset = AssetInfo.from_api_response(data)
|
|
assert not asset.is_processed
|
|
|
|
def test_trashed_asset(self):
|
|
data = {
|
|
"id": "asset-4",
|
|
"type": "IMAGE",
|
|
"originalFileName": "deleted.jpg",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
"isTrashed": True,
|
|
"thumbhash": "abc",
|
|
}
|
|
asset = AssetInfo.from_api_response(data)
|
|
assert not asset.is_processed
|
|
|
|
def test_people_extraction(self):
|
|
data = {
|
|
"id": "asset-5",
|
|
"type": "IMAGE",
|
|
"originalFileName": "group.jpg",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
"thumbhash": "abc",
|
|
"people": [
|
|
{"name": "Alice"},
|
|
{"name": "Bob"},
|
|
{"name": ""}, # empty name filtered
|
|
],
|
|
}
|
|
asset = AssetInfo.from_api_response(data)
|
|
assert asset.people == ["Alice", "Bob"]
|
|
|
|
|
|
class TestAlbumData:
|
|
def test_from_api_response(self):
|
|
data = {
|
|
"id": "album-1",
|
|
"albumName": "Vacation",
|
|
"assetCount": 2,
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z",
|
|
"shared": True,
|
|
"owner": {"name": "Alice"},
|
|
"albumThumbnailAssetId": "asset-1",
|
|
"assets": [
|
|
{
|
|
"id": "asset-1",
|
|
"type": "IMAGE",
|
|
"originalFileName": "photo.jpg",
|
|
"fileCreatedAt": "2024-01-15T10:30:00Z",
|
|
"ownerId": "user-1",
|
|
"thumbhash": "abc",
|
|
},
|
|
{
|
|
"id": "asset-2",
|
|
"type": "VIDEO",
|
|
"originalFileName": "video.mp4",
|
|
"fileCreatedAt": "2024-01-15T11:00:00Z",
|
|
"ownerId": "user-1",
|
|
"thumbhash": "def",
|
|
},
|
|
],
|
|
}
|
|
album = AlbumData.from_api_response(data)
|
|
assert album.id == "album-1"
|
|
assert album.name == "Vacation"
|
|
assert album.photo_count == 1
|
|
assert album.video_count == 1
|
|
assert album.shared
|
|
assert len(album.asset_ids) == 2
|
|
assert "asset-1" in album.asset_ids
|
|
|
|
|
|
class TestAlbumChange:
|
|
def test_basic_creation(self):
|
|
change = AlbumChange(
|
|
album_id="album-1",
|
|
album_name="Test",
|
|
change_type="assets_added",
|
|
added_count=3,
|
|
)
|
|
assert change.added_count == 3
|
|
assert change.removed_count == 0
|
|
assert change.old_name is None
|