refactor: restructure core→server, split large entity files into database module
- Move src/core/ → src/server/ - Split SerieList.py (531 lines) and series.py (414 lines) into src/server/database/ - Add database/models.py for SQLAlchemy models - Update all test imports to reflect new structure - Remove deprecated test files (test_serie_class.py, test_serie_folder_with_year.py)
This commit is contained in:
@@ -6,13 +6,33 @@ special characters, Unicode names, and malformed folder structures.
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.entities.series import Serie
|
||||
from src.core.providers.base_provider import Loader
|
||||
from src.core.SerieScanner import SerieScanner
|
||||
from src.server.database.models import AnimeSeries
|
||||
from src.server.providers.base_provider import Loader
|
||||
from src.server.SerieScanner import SerieScanner
|
||||
from src.server.utils.filesystem import sanitize_folder_name
|
||||
|
||||
|
||||
def make_anime(key, name, folder=None, episode_dict=None, year=None, site="aniworld.to"):
|
||||
"""Create a mock AnimeSeries with needed properties."""
|
||||
anime = MagicMock(spec=AnimeSeries)
|
||||
anime.key = key
|
||||
anime.name = name
|
||||
anime.folder = folder or name
|
||||
anime.site = site
|
||||
anime.year = year
|
||||
anime.episodeDict = episode_dict or {}
|
||||
# Compute name_with_year
|
||||
if year:
|
||||
anime.name_with_year = f"{name} ({year})"
|
||||
else:
|
||||
anime.name_with_year = name
|
||||
# Compute sanitized_folder
|
||||
anime.sanitized_folder = sanitize_folder_name(anime.name_with_year)
|
||||
return anime
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -133,112 +153,112 @@ class TestSpecialCharacters:
|
||||
|
||||
def test_colon_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with colon."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="re-zero",
|
||||
name="Re:Zero - Starting Life in Another World",
|
||||
site="aniworld.to",
|
||||
folder="Re Zero",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
# Sanitized folder should remove colon
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert ":" not in sanitized
|
||||
assert "Re" in sanitized
|
||||
assert "Zero" in sanitized
|
||||
|
||||
def test_slash_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with slash."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="fate-stay-night",
|
||||
name="Fate/Stay Night: Unlimited Blade Works",
|
||||
site="aniworld.to",
|
||||
folder="Fate Stay Night",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "/" not in sanitized
|
||||
assert "\\" not in sanitized
|
||||
|
||||
def test_question_mark_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with question mark."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="is-it-wrong",
|
||||
name="Is It Wrong to Try to Pick Up Girls in a Dungeon?",
|
||||
site="aniworld.to",
|
||||
folder="Is It Wrong",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "?" not in sanitized
|
||||
|
||||
def test_asterisk_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with asterisk."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Series * Special",
|
||||
site="aniworld.to",
|
||||
folder="Series Special",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "*" not in sanitized
|
||||
|
||||
def test_pipe_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with pipe character."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Series | Part 2",
|
||||
site="aniworld.to",
|
||||
folder="Series Part 2",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "|" not in sanitized
|
||||
|
||||
def test_quotes_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with quotes."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name='Series "Subtitle" Edition',
|
||||
site="aniworld.to",
|
||||
folder="Series Subtitle Edition",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Quotes should be removed or replaced
|
||||
assert '"' not in sanitized or sanitized.count('"') == 0
|
||||
|
||||
def test_less_greater_than_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with < and >."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Series <Special> Edition",
|
||||
site="aniworld.to",
|
||||
folder="Series Special Edition",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "<" not in sanitized
|
||||
assert ">" not in sanitized
|
||||
|
||||
def test_multiple_special_chars(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with multiple special characters."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="complex",
|
||||
name="Re:Zero / Fate * Special? <Edition>",
|
||||
site="aniworld.to",
|
||||
folder="Re Zero Fate Special Edition",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Should remove all special chars
|
||||
invalid_chars = [':', '/', '*', '?', '<', '>']
|
||||
for char in invalid_chars:
|
||||
@@ -250,45 +270,45 @@ class TestMultipleSpaces:
|
||||
|
||||
def test_double_spaces(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with double spaces."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Attack on Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Multiple spaces should be preserved or normalized to single space
|
||||
assert "Attack" in sanitized
|
||||
assert "Titan" in sanitized
|
||||
|
||||
def test_leading_trailing_spaces(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with leading/trailing spaces."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name=" Attack on Titan ",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Leading/trailing spaces should be stripped
|
||||
assert not sanitized.startswith(" ")
|
||||
assert not sanitized.endswith(" ")
|
||||
|
||||
def test_tabs_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with tab characters."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Attack\ton\tTitan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Tabs should be handled (removed or replaced)
|
||||
assert "\t" not in sanitized or sanitized.replace("\t", " ")
|
||||
|
||||
@@ -298,95 +318,95 @@ class TestUnicodeNames:
|
||||
|
||||
def test_japanese_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name in Japanese."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="shingeki",
|
||||
name="進撃の巨人",
|
||||
site="aniworld.to",
|
||||
folder="進撃の巨人",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Unicode should be preserved
|
||||
assert "進撃の巨人" in sanitized
|
||||
|
||||
def test_chinese_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name in Chinese."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="进击的巨人",
|
||||
site="aniworld.to",
|
||||
folder="进击的巨人",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "进击的巨人" in sanitized
|
||||
|
||||
def test_korean_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name in Korean."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="진격의 거인",
|
||||
site="aniworld.to",
|
||||
folder="진격의 거인",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "진격의" in sanitized
|
||||
|
||||
def test_arabic_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name in Arabic."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="هجوم العمالقة",
|
||||
site="aniworld.to",
|
||||
folder="هجوم العمالقة",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "هجوم" in sanitized
|
||||
|
||||
def test_cyrillic_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name in Cyrillic."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Атака Титанов",
|
||||
site="aniworld.to",
|
||||
folder="Атака Титанов",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "Атака" in sanitized
|
||||
|
||||
def test_mixed_languages(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with mixed languages."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Attack on Titan - 進撃の巨人",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "Attack" in sanitized
|
||||
assert "進撃の巨人" in sanitized
|
||||
|
||||
def test_emoji_in_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test series name with emoji."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Series ⚔️ Special",
|
||||
site="aniworld.to",
|
||||
folder="Series Special",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Emoji should be handled gracefully
|
||||
assert "Series" in sanitized
|
||||
|
||||
@@ -418,16 +438,16 @@ class TestMalformedFolderStructures:
|
||||
def test_very_long_folder_name(self, temp_anime_dir, mock_loader):
|
||||
"""Test handling of very long folder names."""
|
||||
long_name = "A" * 300 # Very long name
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="long",
|
||||
name=long_name,
|
||||
site="aniworld.to",
|
||||
folder=long_name,
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
# Should handle long names without error
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert len(sanitized) > 0
|
||||
|
||||
def test_folder_name_with_dots(self, temp_anime_dir, mock_loader):
|
||||
@@ -439,127 +459,80 @@ class TestMalformedFolderStructures:
|
||||
|
||||
def test_folder_name_with_underscores(self, temp_anime_dir, mock_loader):
|
||||
"""Test folder name with underscores."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="series",
|
||||
name="Attack_on_Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack_on_Titan",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Underscores are valid filesystem chars
|
||||
assert "Attack" in sanitized
|
||||
|
||||
|
||||
class TestNameWithYearProperty:
|
||||
"""Test Serie.name_with_year property."""
|
||||
"""Test AnimeSeries.name_with_year property."""
|
||||
|
||||
def test_name_with_year_adds_year(self):
|
||||
"""Test that name_with_year adds year in parentheses."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="dororo",
|
||||
name="Dororo",
|
||||
site="aniworld.to",
|
||||
folder="Dororo",
|
||||
episodeDict={},
|
||||
episode_dict={},
|
||||
year=2025
|
||||
)
|
||||
|
||||
assert serie.name_with_year == "Dororo (2025)"
|
||||
assert anime.name_with_year == "Dororo (2025)"
|
||||
|
||||
def test_name_with_year_no_year(self):
|
||||
"""Test name_with_year without year returns just name."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="dororo",
|
||||
name="Dororo",
|
||||
site="aniworld.to",
|
||||
folder="Dororo",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
assert serie.name_with_year == "Dororo"
|
||||
assert anime.name_with_year == "Dororo"
|
||||
|
||||
def test_name_with_year_used_in_sanitized_folder(self):
|
||||
"""Test that sanitized_folder uses name_with_year."""
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key="attack",
|
||||
name="Attack on Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={},
|
||||
episode_dict={},
|
||||
year=2013
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "(2013)" in sanitized
|
||||
assert "Attack on Titan" in sanitized
|
||||
|
||||
def test_name_with_year_does_not_duplicate(self):
|
||||
"""Test that name_with_year doesn't duplicate year."""
|
||||
serie = Serie(
|
||||
key="eighty-six",
|
||||
name="86 Eighty Six (2021)",
|
||||
site="aniworld.to",
|
||||
folder="86 Eighty Six (2021)",
|
||||
episodeDict={},
|
||||
year=2021
|
||||
)
|
||||
|
||||
assert serie.name_with_year == "86 Eighty Six (2021)"
|
||||
assert serie.name_with_year.count("(2021)") == 1
|
||||
|
||||
class TestSanitizedFolder:
|
||||
"""Test AnimeSeries.sanitized_folder property."""
|
||||
|
||||
class TestEnsureFolderWithYear:
|
||||
"""Test Serie.ensure_folder_with_year method."""
|
||||
|
||||
def test_ensure_folder_adds_year_when_missing(self):
|
||||
"""Test that ensure_folder_with_year adds year to folder."""
|
||||
serie = Serie(
|
||||
def test_sanitized_folder_uses_name_with_year(self):
|
||||
"""Test that sanitized_folder uses name_with_year."""
|
||||
anime = make_anime(
|
||||
key="attack",
|
||||
name="Attack on Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={},
|
||||
episode_dict={},
|
||||
year=2013
|
||||
)
|
||||
|
||||
result = serie.ensure_folder_with_year()
|
||||
|
||||
assert "(2013)" in result
|
||||
assert serie.folder == result
|
||||
|
||||
def test_ensure_folder_doesnt_duplicate_year(self):
|
||||
"""Test that year isn't added if already present."""
|
||||
serie = Serie(
|
||||
key="attack",
|
||||
name="Attack on Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan (2013)",
|
||||
episodeDict={},
|
||||
year=2013
|
||||
)
|
||||
|
||||
original_folder = serie.folder
|
||||
result = serie.ensure_folder_with_year()
|
||||
|
||||
# Should not change
|
||||
assert result.count("(2013)") == 1
|
||||
|
||||
def test_ensure_folder_no_year_unchanged(self):
|
||||
"""Test that folder unchanged when no year available."""
|
||||
serie = Serie(
|
||||
key="attack",
|
||||
name="Attack on Titan",
|
||||
site="aniworld.to",
|
||||
folder="Attack on Titan",
|
||||
episodeDict={}
|
||||
)
|
||||
|
||||
original_folder = serie.folder
|
||||
result = serie.ensure_folder_with_year()
|
||||
|
||||
assert result == original_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
assert "(2013)" in sanitized
|
||||
assert "Attack on Titan" in sanitized
|
||||
|
||||
|
||||
class TestRealWorldScenarios:
|
||||
@@ -576,15 +549,15 @@ class TestRealWorldScenarios:
|
||||
]
|
||||
|
||||
for key, name, expected_part in test_cases:
|
||||
serie = Serie(
|
||||
anime = make_anime(
|
||||
key=key,
|
||||
name=name,
|
||||
site="aniworld.to",
|
||||
folder="old-folder",
|
||||
episodeDict={}
|
||||
episode_dict={}
|
||||
)
|
||||
|
||||
sanitized = serie.sanitized_folder
|
||||
sanitized = anime.sanitized_folder
|
||||
# Check that expected part is in sanitized name
|
||||
assert any(word in sanitized for word in expected_part.split())
|
||||
# Check invalid chars removed (< > : " / \ | ? *)
|
||||
|
||||
Reference in New Issue
Block a user