diff --git a/backend/src/test/test_issue_bugs.py b/backend/src/test/test_issue_bugs.py index 18322f9e..a0af3420 100644 --- a/backend/src/test/test_issue_bugs.py +++ b/backend/src/test/test_issue_bugs.py @@ -288,3 +288,174 @@ class TestIssue974FilterPatternError: p1 = engine._get_filter_pattern("720,1080") p2 = engine._get_filter_pattern("720,1080") assert p1 is p2 + + +# --------------------------------------------------------------------------- +# Issue #990: Titles starting with numbers cause title_raw=None, crashing +# the RSS loop with TypeError in match_torrent +# https://github.com/EstrellaXD/Auto_Bangumi/issues/990 +# +# "[ANi] 29 岁单身中坚冒险家的日常 - 07" → regex matches "29 " as episode, +# title becomes empty → title_raw=None → None stored as alias → crash on +# `None in torrent_name` in match_torrent. +# --------------------------------------------------------------------------- + + +class TestIssue990NumberPrefixTitle: + """Issue #990: Titles starting with numbers crash RSS loop.""" + + PROBLEM_TITLE = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]" + + def test_raw_parser_misparses_leading_number_as_episode(self): + """raw_parser matches leading '29 ' as episode number, losing the title.""" + result = raw_parser(self.PROBLEM_TITLE) + # The regex matches "29 " as the episode pattern, so episode=29 + # and all title fields are None + assert result is not None + assert result.episode == 29 + assert result.title_en is None + assert result.title_zh is None + assert result.title_jp is None + + def test_title_parser_returns_none_when_title_raw_empty(self): + """TitleParser.raw_parser returns None when no title can be extracted.""" + from module.parser.title_parser import TitleParser + + result = TitleParser.raw_parser(self.PROBLEM_TITLE) + # Should return None instead of a Bangumi with title_raw=None + assert result is None + + def test_add_title_alias_rejects_none(self, db_session): + """add_title_alias should reject None as alias.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + + db = BangumiDatabase(db_session) + bangumi = Bangumi( + official_title="29岁单身冒险家的日常", + title_raw="[ANi] 29岁单身冒险家的日常", + season=1, + ) + db_session.add(bangumi) + db_session.commit() + + result = db.add_title_alias(bangumi.id, None) + assert result is False + # Verify no alias was stored + assert bangumi.title_aliases is None + + def test_add_title_alias_rejects_empty_string(self, db_session): + """add_title_alias should reject empty string as alias.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + + db = BangumiDatabase(db_session) + bangumi = Bangumi( + official_title="Test Anime", + title_raw="[Group] Test Anime", + season=1, + ) + db_session.add(bangumi) + db_session.commit() + + result = db.add_title_alias(bangumi.id, "") + assert result is False + + def test_get_aliases_list_filters_null_values(self): + """_get_aliases_list should filter out null values from JSON.""" + from module.database.bangumi import _get_aliases_list + from module.models import Bangumi + + bangumi = Bangumi(title_raw="test", official_title="Test") + # Simulates the corrupted state: [null] stored in DB + bangumi.title_aliases = "[null]" + assert _get_aliases_list(bangumi) == [] + + # Mixed valid and null values + bangumi.title_aliases = '[null, "valid_alias", null, "another"]' + assert _get_aliases_list(bangumi) == ["valid_alias", "another"] + + def test_get_all_title_patterns_skips_none_title_raw(self, db_session): + """get_all_title_patterns should return empty list when title_raw is None.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + + db = BangumiDatabase(db_session) + bangumi = Bangumi(official_title="Test Anime") + bangumi.title_raw = None + bangumi.title_aliases = None + + patterns = db.get_all_title_patterns(bangumi) + assert patterns == [] + + def test_match_torrent_no_crash_on_none_title_raw(self, db_session): + """match_torrent should not crash when a bangumi has None title_raw.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + + db = BangumiDatabase(db_session) + # Insert a bangumi with corrupted title_raw (simulating the bug state) + bangumi = Bangumi( + official_title="29岁单身冒险家的日常", + season=1, + ) + bangumi.title_raw = None + db_session.add(bangumi) + db_session.commit() + + # Should not raise TypeError: 'in ' requires string + result = db.match_torrent( + "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL]" + ) + assert result is None + + def test_match_torrent_no_crash_on_null_aliases(self, db_session): + """match_torrent should not crash when title_aliases contains null.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + + db = BangumiDatabase(db_session) + bangumi = Bangumi( + official_title="29岁单身冒险家的日常", + title_raw="[ANi] 29岁单身冒险家的日常", + season=1, + ) + bangumi.title_aliases = "[null]" + db_session.add(bangumi) + db_session.commit() + + # Should not crash — null aliases are filtered out + result = db.match_torrent( + "[ANi] 29岁单身冒险家的日常 - 07 [1080P][Baha][WEB-DL]" + ) + assert result is not None + assert result.official_title == "29岁单身冒险家的日常" + + def test_match_list_no_crash_on_corrupted_data(self, db_session): + """match_list should handle bangumi with None title_raw and null aliases.""" + from module.database.bangumi import BangumiDatabase + from module.models import Bangumi + from unittest.mock import MagicMock + + db = BangumiDatabase(db_session) + + # Insert corrupted bangumi (title_raw=None, aliases=[null]) + bangumi = Bangumi(official_title="29岁单身冒险家的日常", season=1) + bangumi.title_raw = None + bangumi.title_aliases = "[null]" + db_session.add(bangumi) + + # Insert a valid bangumi + valid = Bangumi( + official_title="葬送的芙莉莲", + title_raw="葬送的芙莉莲 / Sousou no Frieren", + season=1, + ) + db_session.add(valid) + db_session.commit() + + torrent = MagicMock() + torrent.name = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P]" + + # Should not crash even with corrupted data in the DB + unmatched = db.match_list([torrent], "https://mikanani.me/RSS/test")