Files
alfred/tests/domain/test_shared_value_objects.py
T
francwa 9556bf9e08 refactor(domain): strip live filesystem I/O from VOs and entities
DDD-pure cleanup — entities and value objects no longer query the world
at read time.

  FilePath: drop .exists() / .is_file() / .is_dir(). The VO is now a
    pure address; ask the injected FilesystemScanner for live state.
  Movie:    drop .has_file() / .is_downloaded(). Invariant: when the
    application sets file_path, it has already constated the file
    exists; downstream readers trust the snapshot.
  Episode:  same — drop .has_file() / .is_downloaded().
  SubtitlePlacer: drop the pre-check .exists() calls. The placer now
    attempts os.link() and reports FileNotFoundError / FileExistsError
    as skip reasons. Removes a TOCTOU race as a bonus.

Tests adjusted: the FilePath VO method tests are gone (the methods are
gone), test_has_file_false_when_no_path replaced by a plain assertion
on file_path is None. Placer tests are unchanged — the skip-reason
strings ('not found', 'already exists') match the new try/except paths.

The 'snapshot value objects' pattern (ProbedMediaInfo, TmdbMovieInfo)
that this cleanup enables is documented in refactor_domain_io.md, to
be applied when a future use case actually needs richer metadata —
not now, no speculative VOs.
2026-05-19 14:58:59 +02:00

120 lines
3.4 KiB
Python

"""Tests for shared domain value objects: ImdbId, FilePath, FileSize."""
from pathlib import Path
import pytest
from alfred.domain.shared.exceptions import ValidationError
from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId
# ---------------------------------------------------------------------------
# ImdbId
# ---------------------------------------------------------------------------
class TestImdbId:
def test_valid_7_digits(self):
id_ = ImdbId("tt1375666")
assert str(id_) == "tt1375666"
def test_valid_8_digits(self):
id_ = ImdbId("tt12345678")
assert str(id_) == "tt12345678"
def test_empty_raises(self):
with pytest.raises(ValidationError):
ImdbId("")
def test_no_tt_prefix_raises(self):
with pytest.raises(ValidationError):
ImdbId("1375666")
def test_too_few_digits_raises(self):
with pytest.raises(ValidationError):
ImdbId("tt12345") # only 5 digits
def test_too_many_digits_raises(self):
with pytest.raises(ValidationError):
ImdbId("tt123456789") # 9 digits
def test_non_string_raises(self):
with pytest.raises(ValidationError):
ImdbId(1375666) # type: ignore
def test_repr(self):
assert "tt1375666" in repr(ImdbId("tt1375666"))
def test_equality(self):
assert ImdbId("tt1375666") == ImdbId("tt1375666")
assert ImdbId("tt1375666") != ImdbId("tt0903747")
def test_hashable(self):
# Frozen dataclass should be hashable
ids = {ImdbId("tt1375666"), ImdbId("tt0903747")}
assert len(ids) == 2
# ---------------------------------------------------------------------------
# FilePath
# ---------------------------------------------------------------------------
class TestFilePath:
def test_from_string(self, tmp_path):
p = FilePath(str(tmp_path))
assert isinstance(p.value, Path)
def test_from_path(self, tmp_path):
p = FilePath(tmp_path)
assert p.value == tmp_path
def test_invalid_type_raises(self):
with pytest.raises(ValidationError):
FilePath(123) # type: ignore
def test_str(self, tmp_path):
p = FilePath(tmp_path)
assert str(p) == str(tmp_path)
# ---------------------------------------------------------------------------
# FileSize
# ---------------------------------------------------------------------------
class TestFileSize:
def test_bytes(self):
s = FileSize(500)
assert s.bytes == 500
def test_negative_raises(self):
with pytest.raises(ValidationError):
FileSize(-1)
def test_non_integer_raises(self):
with pytest.raises(ValidationError):
FileSize(1.5) # type: ignore
def test_zero_is_valid(self):
s = FileSize(0)
assert s.bytes == 0
def test_human_readable_bytes(self):
assert FileSize(500).to_human_readable() == "500 B"
def test_human_readable_kb(self):
result = FileSize(2048).to_human_readable()
assert "KB" in result
def test_human_readable_mb(self):
result = FileSize(5 * 1024 * 1024).to_human_readable()
assert "MB" in result
def test_human_readable_gb(self):
result = FileSize(2 * 1024**3).to_human_readable()
assert "GB" in result
def test_str_is_human_readable(self):
s = FileSize(1024)
assert str(s) == s.to_human_readable()