Files
haos-hacs-immich-album-watcher/packages/core/tests/test_models.py
alexei.dolgolyov d0783d0b6a
Some checks failed
Validate / Hassfest (push) Has been cancelled
Add shared core library and architecture plans (Phase 1)
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>
2026-03-19 12:40:08 +03:00

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