From 7ac7039592dfef9742a744c09ca2d5ac48fcd615 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Mon, 4 May 2026 22:23:19 +0300 Subject: [PATCH] chore: tests clean up --- .../conditions/tests/test_conditions.py | 2 +- .../presets/tests/test_presets_repository.py | 16 +- app/features/streaming/tests/test_ffprobe.py | 4 +- app/features/streaming/tests/test_m3u8.py | 4 +- app/features/streaming/tests/test_playlist.py | 2 +- app/features/streaming/tests/test_segments.py | 4 +- app/features/streaming/tests/test_subtitle.py | 10 +- .../streaming/tests/test_subtitle_routes.py | 12 +- .../tests/test_generic_task_handler.py | 103 ++++----- .../definitions/tests/test_tver_handler.py | 4 +- .../ytdlp/tests/test_ytdlp_extractor.py | 4 +- .../ytdlp/tests/test_ytdlp_mini_filter.py | 4 +- app/features/ytdlp/tests/test_ytdlp_module.py | 50 ++--- app/features/ytdlp/tests/test_ytdlp_opts.py | 24 +-- app/features/ytdlp/tests/test_ytdlp_utils.py | 67 +++--- app/tests/test_cache.py | 2 +- app/tests/test_condition_ignore.py | 14 +- app/tests/test_datastore.py | 18 +- app/tests/test_download.py | 79 ++++--- app/tests/test_download_utils.py | 24 +-- app/tests/test_encoder.py | 2 +- app/tests/test_events.py | 6 +- app/tests/test_history_routes.py | 14 +- app/tests/test_httpx_client.py | 8 +- app/tests/test_itemdto.py | 18 +- app/tests/test_package_installer.py | 22 +- app/tests/test_router.py | 4 +- app/tests/test_scheduler.py | 4 +- app/tests/test_services.py | 2 +- app/tests/test_sqlite_store.py | 14 +- app/tests/test_static_routes.py | 12 +- app/tests/test_system_routes.py | 4 +- app/tests/test_terminal_session_manager.py | 24 +-- app/tests/test_test_helpers.py | 6 +- app/tests/test_update_checker.py | 8 +- app/tests/test_utils.py | 198 ++++++++---------- ui/tests/composables/useConditions.test.ts | 24 +-- .../composables/useConsoleSession.test.ts | 27 ++- .../composables/useDirtyCloseGuard.test.ts | 10 +- .../composables/usePlayerShortcuts.test.ts | 4 +- .../composables/usePlayerSubtitles.test.ts | 8 +- ui/tests/composables/usePresetOptions.test.ts | 4 +- ui/tests/composables/usePresets.test.ts | 24 +-- .../composables/useTaskDefinitions.test.ts | 12 +- ui/tests/composables/useTasks.test.ts | 74 +++---- ui/tests/plugins/modal-opacity.client.test.ts | 8 +- ui/tests/utils/index.test.ts | 102 ++++----- ui/tests/utils/sidebarSwipe.test.ts | 8 +- ui/tests/utils/ytdlp.test.ts | 32 ++- 49 files changed, 535 insertions(+), 595 deletions(-) diff --git a/app/features/conditions/tests/test_conditions.py b/app/features/conditions/tests/test_conditions.py index f72ec622..0586404f 100644 --- a/app/features/conditions/tests/test_conditions.py +++ b/app/features/conditions/tests/test_conditions.py @@ -53,7 +53,7 @@ class TestAllowInternalUrlsScope: Config._reset_singleton() @pytest.mark.asyncio - async def test_conditions_test_rejects_internal_url_when_disallowed(self) -> None: + async def test_rejects_internal_url(self) -> None: config = Config.get_instance() config.allow_internal_urls = False encoder = Encoder() diff --git a/app/features/presets/tests/test_presets_repository.py b/app/features/presets/tests/test_presets_repository.py index 1dfe6746..5b26de4a 100644 --- a/app/features/presets/tests/test_presets_repository.py +++ b/app/features/presets/tests/test_presets_repository.py @@ -99,7 +99,7 @@ class TestPresetsRepository: assert total_pages == 1, "Should compute pages from the filtered total" @pytest.mark.asyncio - async def test_list_paginated_supports_multiple_sort_fields(self, repo): + async def test_list_paginated_multi_sort(self, repo): await repo.create({"name": "Charlie", "priority": 2}) await repo.create({"name": "Alpha", "priority": 1}) await repo.create({"name": "Bravo", "priority": 1}) @@ -113,24 +113,24 @@ class TestPresetsRepository: ], "Should support multiple sort fields and directions" @pytest.mark.asyncio - async def test_list_paginated_rejects_invalid_sort_field(self, repo): + async def test_list_paginated_bad_sort(self, repo): with pytest.raises(ValueError, match="sort must use supported fields"): await repo.list_paginated(page=1, per_page=10, sort="cli", order="asc") @pytest.mark.asyncio - async def test_list_paginated_rejects_invalid_sort_direction(self, repo): + async def test_list_paginated_bad_order(self, repo): with pytest.raises(ValueError, match="order must be 'asc' or 'desc'"): await repo.list_paginated(page=1, per_page=10, sort="name", order="sideways") @pytest.mark.asyncio - async def test_list_paginated_rejects_mismatched_sort_and_order_lengths(self, repo): + async def test_list_paginated_mismatched_order(self, repo): with pytest.raises(ValueError, match="order must provide one direction or match the number of sort fields"): await repo.list_paginated(page=1, per_page=10, sort="priority,name", order="asc,desc,asc") @pytest.mark.asyncio class TestPresetRoutes: - async def test_list_route_supports_sort_params(self, repo): + async def test_list_route_sort(self, repo): await repo.create({"name": "Alpha", "priority": 1}) await repo.create({"name": "Bravo", "priority": 1}) await repo.create({"name": "Charlie", "priority": 2}) @@ -144,7 +144,7 @@ class TestPresetRoutes: assert response.status == web.HTTPOk.status_code, "Should return 200 for valid sorting" assert [item["name"] for item in payload["items"]] == ["bravo", "alpha", "charlie"], "Should sort response" - async def test_list_route_rejects_invalid_sort_field(self, repo): + async def test_list_route_bad_sort(self, repo): request = MagicMock(spec=Request) request.query = {"sort": "cli", "order": "asc"} @@ -154,7 +154,7 @@ class TestPresetRoutes: assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort field" assert "sort" in payload["error"], "Should explain invalid sort field" - async def test_list_route_rejects_invalid_sort_direction(self, repo): + async def test_list_route_bad_order(self, repo): request = MagicMock(spec=Request) request.query = {"sort": "name", "order": "sideways"} @@ -164,7 +164,7 @@ class TestPresetRoutes: assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort direction" assert "order" in payload["error"], "Should explain invalid sort direction" - async def test_list_route_supports_excluding_defaults(self, repo): + async def test_list_route_exclude_defaults(self, repo): await repo.create({"name": "System Default", "default": True, "priority": 10}) await repo.create({"name": "Custom Preset", "priority": 1}) diff --git a/app/features/streaming/tests/test_ffprobe.py b/app/features/streaming/tests/test_ffprobe.py index 621ac12b..5982386b 100644 --- a/app/features/streaming/tests/test_ffprobe.py +++ b/app/features/streaming/tests/test_ffprobe.py @@ -84,13 +84,11 @@ class TestFFProbe: # First call should execute the function result1 = await ffprobe(str(self.test_file)) - assert result1 is not None assert isinstance(result1.metadata, dict) first_call_count = call_count # Second call with same argument should use cached result result2 = await ffprobe(str(self.test_file)) - assert result2 is not None assert isinstance(result2.metadata, dict) assert call_count == first_call_count, ( @@ -119,7 +117,7 @@ class TestFFProbe: # Test with Path object result = await ffprobe(self.test_file) - assert result is not None + assert result.metadata == {"duration": "10.0"} def test_ffprobe_result_properties(self): """Test FFProbeResult object properties.""" diff --git a/app/features/streaming/tests/test_m3u8.py b/app/features/streaming/tests/test_m3u8.py index 2e97da56..12f1d838 100644 --- a/app/features/streaming/tests/test_m3u8.py +++ b/app/features/streaming/tests/test_m3u8.py @@ -64,7 +64,7 @@ async def test_make_stream_basic_ok_codecs(tmp_path: Path, monkeypatch: pytest.M @pytest.mark.asyncio -async def test_make_stream_transcode_flags_and_remainder(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_make_stream_transcode_flags(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: base = tmp_path / "dl" base.mkdir() media = base / "v.mp4" @@ -107,7 +107,7 @@ async def test_make_stream_transcode_flags_and_remainder(tmp_path: Path, monkeyp @pytest.mark.asyncio -async def test_make_stream_raises_without_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_make_stream_no_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: base = tmp_path / "dl" base.mkdir() media = base / "v.mp4" diff --git a/app/features/streaming/tests/test_playlist.py b/app/features/streaming/tests/test_playlist.py index f69025ea..90decebd 100644 --- a/app/features/streaming/tests/test_playlist.py +++ b/app/features/streaming/tests/test_playlist.py @@ -99,7 +99,7 @@ async def test_make_playlist_with_subtitles(tmp_path: Path, monkeypatch: pytest. @pytest.mark.asyncio -async def test_make_playlist_raises_without_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_make_playlist_no_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: base = tmp_path / "downloads" base.mkdir() media = base / "file.mp4" diff --git a/app/features/streaming/tests/test_segments.py b/app/features/streaming/tests/test_segments.py index 57c30e5a..33ec0e03 100644 --- a/app/features/streaming/tests/test_segments.py +++ b/app/features/streaming/tests/test_segments.py @@ -172,7 +172,7 @@ class _FakeProcFail(_FakeProc): @pytest.mark.asyncio -async def test_build_ffmpeg_args_no_dri_falls_back_to_software(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_build_ffmpeg_args_no_dri(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: media = tmp_path / "file.mp4" media.write_bytes(b"data") @@ -219,7 +219,7 @@ async def test_build_ffmpeg_args_no_dri_falls_back_to_software(tmp_path: Path, m @pytest.mark.asyncio -async def test_stream_gpu_failure_falls_back_to_software( +async def test_stream_gpu_fallback( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: async def fake_ffprobe(_file: Path): diff --git a/app/features/streaming/tests/test_subtitle.py b/app/features/streaming/tests/test_subtitle.py index 0c63290f..95426a22 100644 --- a/app/features/streaming/tests/test_subtitle.py +++ b/app/features/streaming/tests/test_subtitle.py @@ -44,7 +44,7 @@ async def test_make_vtt_reads_file(tmp_path: Path) -> None: @pytest.mark.asyncio -async def test_make_delivery_returns_raw_ass_with_ass_content_type(tmp_path: Path) -> None: +async def test_make_delivery_ass(tmp_path: Path) -> None: ass = tmp_path / "file.ass" content = "[Script Info]\nTitle: Demo\n" ass.write_text(content, encoding="utf-8") @@ -55,7 +55,7 @@ async def test_make_delivery_returns_raw_ass_with_ass_content_type(tmp_path: Pat assert media_type == "text/x-ssa; charset=UTF-8" -def test_get_subtitle_tracks_prefers_native_then_ass(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_tracks_order(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: media = tmp_path / "video.mkv" media.write_text("x", encoding="utf-8") ass_file = tmp_path / "video.ass" @@ -107,7 +107,7 @@ async def test_make_no_events_raises(tmp_path: Path) -> None: @pytest.mark.asyncio -async def test_make_single_event_returns_vtt(tmp_path: Path) -> None: +async def test_make_single_vtt(tmp_path: Path) -> None: srt = tmp_path / "sub.ass" srt.write_text("dummy") @@ -122,7 +122,7 @@ async def test_make_single_event_returns_vtt(tmp_path: Path) -> None: @pytest.mark.asyncio -async def test_make_two_events_pop_first_when_ends_equal(tmp_path: Path) -> None: +async def test_make_two_events_same_end(tmp_path: Path) -> None: srt = tmp_path / "sub.srt" srt.write_text("dummy") @@ -138,7 +138,7 @@ async def test_make_two_events_pop_first_when_ends_equal(tmp_path: Path) -> None @pytest.mark.asyncio -async def test_make_two_events_no_pop_when_different(tmp_path: Path) -> None: +async def test_make_two_events_keep_both(tmp_path: Path) -> None: srt = tmp_path / "sub.srt" srt.write_text("dummy") diff --git a/app/features/streaming/tests/test_subtitle_routes.py b/app/features/streaming/tests/test_subtitle_routes.py index 8c319db7..4fc3248b 100644 --- a/app/features/streaming/tests/test_subtitle_routes.py +++ b/app/features/streaming/tests/test_subtitle_routes.py @@ -37,9 +37,7 @@ def _make_request(path: str, *, match_info: dict[str, str]) -> web.Request: @pytest.mark.asyncio -async def test_subtitles_manifest_lists_tracks_in_native_first_order( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_subtitles_manifest_order(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: config = Config.get_instance() config.download_path = str(tmp_path) @@ -79,7 +77,6 @@ async def test_subtitles_manifest_lists_tracks_in_native_first_order( response = await router.subtitles_manifest_get(req, config, req.app) assert response.status == web.HTTPOk.status_code - assert response.body is not None body = response.text assert '"source_format": "vtt"' in body assert '"renderer": "native"' in body @@ -89,7 +86,7 @@ async def test_subtitles_manifest_lists_tracks_in_native_first_order( @pytest.mark.asyncio -async def test_subtitles_track_get_returns_raw_ass_delivery(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_subtitles_track_get_ass(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: config = Config.get_instance() config.download_path = str(tmp_path) @@ -113,9 +110,7 @@ async def test_subtitles_track_get_returns_raw_ass_delivery(tmp_path: Path, monk @pytest.mark.asyncio -async def test_subtitles_track_get_rejects_mismatched_source_format( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_subtitles_track_bad_format(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: config = Config.get_instance() config.download_path = str(tmp_path) @@ -134,5 +129,4 @@ async def test_subtitles_track_get_rejects_mismatched_source_format( response = await router.subtitles_track_get(req, config, req.app) assert response.status == web.HTTPBadRequest.status_code - assert response.body is not None assert b"does not match requested source format" in response.body diff --git a/app/features/tasks/definitions/tests/test_generic_task_handler.py b/app/features/tasks/definitions/tests/test_generic_task_handler.py index eda8392e..d06b3d67 100644 --- a/app/features/tasks/definitions/tests/test_generic_task_handler.py +++ b/app/features/tasks/definitions/tests/test_generic_task_handler.py @@ -21,7 +21,7 @@ def reset_generic_handler(monkeypatch): monkeypatch.setattr(GenericTaskHandler, "_sources_mtime", {}) -def test_build_task_definition_parses_valid_payload(): +def test_build_def_payload(): definition = TaskDefinition( id=1, name="example", @@ -40,16 +40,12 @@ def test_build_task_definition_parses_valid_payload(): ), ) - assert definition is not None, "TaskDefinition should be created" - assert "example" == definition.name, "Name should match" - assert "https://example.com/articles/*" in definition.match_url, "Match URL should be in list" - assert "link" in definition.definition.parse, "Parse should contain link field" - assert ".article a.link::attr(href)" == definition.definition.parse["link"]["expression"], ( - "Link expression should match" - ) + assert definition.name == "example" + assert definition.match_url == ["https://example.com/articles/*"] + assert definition.definition.parse["link"]["expression"] == ".article a.link::attr(href)" -def test_build_task_definition_handles_container(): +def test_build_def_container(): definition = TaskDefinition( id=2, name="container", @@ -73,13 +69,11 @@ def test_build_task_definition_handles_container(): ), ) - assert definition is not None, "TaskDefinition should be created" - assert "items" in definition.definition.parse, "Parse should contain items container" - assert ".cards .card" == definition.definition.parse["items"]["selector"], "Items selector should match" - assert "link" in definition.definition.parse["items"]["fields"], "Items fields should contain link" + assert definition.definition.parse["items"]["selector"] == ".cards .card" + assert definition.definition.parse["items"]["fields"]["link"]["attribute"] == "href" -def test_build_task_definition_handles_json(): +def test_build_def_json(): definition = TaskDefinition( id=3, name="json-def", @@ -104,16 +98,12 @@ def test_build_task_definition_handles_json(): ), ) - assert definition is not None, "TaskDefinition should be created" - assert "json" == definition.definition.response.type, "Response type should be json" - assert "items" in definition.definition.parse, "Parse should contain items container" - assert "jsonpath" == definition.definition.parse["items"]["type"], "Items type should be jsonpath" - assert "jsonpath" == definition.definition.parse["items"]["fields"]["link"]["type"], ( - "Link field type should be jsonpath" - ) + assert definition.definition.response.type == "json" + assert definition.definition.parse["items"]["type"] == "jsonpath" + assert definition.definition.parse["items"]["fields"]["link"]["type"] == "jsonpath" -def test_parse_items_extracts_values(): +def test_parse_items_basic(): definition = TaskDefinition( id=4, name="example", @@ -146,14 +136,16 @@ def test_parse_items_extracts_values(): items = GenericTaskHandler._parse_items(definition, html, "https://example.com/base/") - assert 2 == len(items), "Should extract 2 items" - assert "https://example.com/article-101" == items[0]["link"], "First item link should be absolute URL" - assert "First Title" == items[0]["title"], "First item title should match" - assert "101" == items[0]["id"], "First item id should match" - assert "https://example.com/article-102" == items[1]["link"], "Second item link should match" + assert len(items) == 2 + assert items[0] == { + "link": "https://example.com/article-101", + "title": "First Title", + "id": "101", + } + assert items[1]["link"] == "https://example.com/article-102" -def test_parse_items_handles_nested_card_layout(): +def test_parse_items_cards(): definition = TaskDefinition( id=5, name="nested", @@ -234,19 +226,21 @@ def test_parse_items_handles_nested_card_layout(): items = GenericTaskHandler._parse_items(definition, html, "https://example.com") - assert 2 == len(items), "Should extract 2 items" - assert "https://example.com/poems/view/111" == items[0]["link"], "First item link should match" - assert "First Poem" == items[0]["title"], "First item title should match" - assert "Poet Alpha" == items[0]["poet"], "First item poet should match" - assert "Category One" == items[0]["category"], "First item category should match" - - assert "https://example.com/poems/view/222" == items[1]["link"], "Second item link should match" - assert "Second Poem" == items[1]["title"], "Second item title should match" - assert "Poet Beta" == items[1]["poet"], "Second item poet should match" - assert "category" not in items[1], "Second item should not have category" + assert len(items) == 2 + assert items[0] == { + "link": "https://example.com/poems/view/111", + "title": "First Poem", + "poet": "Poet Alpha", + "category": "Category One", + } + assert items[1] == { + "link": "https://example.com/poems/view/222", + "title": "Second Poem", + "poet": "Poet Beta", + } -def test_parse_items_handles_json_container(): +def test_parse_items_json(): definition = TaskDefinition( id=6, name="json", @@ -287,14 +281,10 @@ def test_parse_items_handles_json_container(): json_data=payload, ) - assert 2 == len(items), "Should extract 2 items (third missing link)" - assert "https://example.com/video/1" == items[0]["link"], "First item link should be absolute" - assert "First" == items[0]["title"], "First item title should match" - assert "1" == items[0]["id"], "First item id should match" - - assert "https://example.com/video/2" == items[1]["link"], "Second item link should match" - assert "Second" == items[1]["title"], "Second item title should match" - assert "2" == items[1]["id"], "Second item id should match" + assert items == [ + {"link": "https://example.com/video/1", "title": "First", "id": "1"}, + {"link": "https://example.com/video/2", "title": "Second", "id": "2"}, + ] @pytest.mark.asyncio @@ -345,14 +335,14 @@ async def test_generic_task_handler_inspect(monkeypatch): task = HandleTask(id=1, name="Inspect", url="https://example.com/api") result: TaskResult | TaskFailure = await GenericTaskHandler.extract(task) - assert isinstance(result, TaskResult), "Result should be TaskResult" - assert 1 == len(result.items), "Should have 1 item" + assert isinstance(result, TaskResult) + assert len(result.items) == 1 item = result.items[0] - assert "https://example.com/video/1" == item.url, "Item URL should match" - assert "First" == item.title, "Item title should match" + assert item.url == "https://example.com/video/1" + assert item.title == "First" -def test_parse_items_handles_json_top_level_list(): +def test_parse_items_json_list(): definition = TaskDefinition( id=8, name="json-list", @@ -389,8 +379,7 @@ def test_parse_items_handles_json_top_level_list(): json_data=payload, ) - assert 2 == len(items), "Should extract 2 items" - assert "https://example.com/video/1" == items[0]["link"], "First item link should match" - assert "First" == items[0]["title"], "First item title should match" - assert "https://example.com/video/2" == items[1]["link"], "Second item link should match" - assert "Second" == items[1]["title"], "Second item title should match" + assert items == [ + {"link": "https://example.com/video/1", "title": "First"}, + {"link": "https://example.com/video/2", "title": "Second"}, + ] diff --git a/app/features/tasks/definitions/tests/test_tver_handler.py b/app/features/tasks/definitions/tests/test_tver_handler.py index b2179c1a..d5711728 100644 --- a/app/features/tasks/definitions/tests/test_tver_handler.py +++ b/app/features/tasks/definitions/tests/test_tver_handler.py @@ -142,7 +142,7 @@ async def test_tver_handler_extract(monkeypatch): @pytest.mark.parametrize(("url", "should_match"), TverHandler.tests()) -def test_tver_handler_parse(url: str, should_match: bool): +def test_parse(url: str, should_match: bool): """Test tver URL parsing.""" result = TverHandler.parse(url) if should_match: @@ -153,7 +153,7 @@ def test_tver_handler_parse(url: str, should_match: bool): @pytest.mark.asyncio -async def test_tver_handler_can_handle(): +async def test_can_handle(): """Test tver handler can_handle method.""" task_valid = HandleTask(id=1, name="Test", url="https://tver.jp/series/sr8sb9pnhc", preset="default") task_invalid = HandleTask(id=2, name="Test", url="https://youtube.com/watch?v=123", preset="default") diff --git a/app/features/ytdlp/tests/test_ytdlp_extractor.py b/app/features/ytdlp/tests/test_ytdlp_extractor.py index 557e0bc7..75381ddd 100644 --- a/app/features/ytdlp/tests/test_ytdlp_extractor.py +++ b/app/features/ytdlp/tests/test_ytdlp_extractor.py @@ -20,7 +20,7 @@ class TestProcessPoolConfiguration: assert kwargs == {"mp_context": context} get_context.assert_called_once_with("fork") - def test_uses_default_context_when_not_frozen_linux(self, monkeypatch): + def test_default_context_linux(self, monkeypatch): monkeypatch.setattr("app.features.ytdlp.extractor.sys.platform", "linux") monkeypatch.delattr("app.features.ytdlp.extractor.sys.frozen", raising=False) @@ -30,7 +30,7 @@ class TestProcessPoolConfiguration: assert _get_process_pool_kwargs() == {} get_context.assert_not_called() - def test_initializes_process_pool_with_context_kwargs(self, monkeypatch): + def test_init_pool_kwargs(self, monkeypatch): context = object() monkeypatch.setattr("app.features.ytdlp.extractor._get_process_pool_kwargs", lambda: {"mp_context": context}) diff --git a/app/features/ytdlp/tests/test_ytdlp_mini_filter.py b/app/features/ytdlp/tests/test_ytdlp_mini_filter.py index 307a9515..66b31824 100644 --- a/app/features/ytdlp/tests/test_ytdlp_mini_filter.py +++ b/app/features/ytdlp/tests/test_ytdlp_mini_filter.py @@ -195,7 +195,7 @@ class TestMiniFilter(unittest.TestCase): self._test("filesize>=1GiB", {"filesize": 2**30}, expected_result=True, test_name="filesize_1gib_positive") self._test("filesize>=1GiB", {"filesize": 1000000}, expected_result=False, test_name="filesize_1gib_negative") - def test_complex_duration_units_with_or_and_grouping(self): + def test_duration_or_grouping(self): """Test complex expressions with duration units, OR operations, and grouping. Skip yt-dlp due to known parsing bugs.""" # Test grouping with duration units expr = "(filesize>1MB & duration<10m) || uploader='BBC'" @@ -310,7 +310,7 @@ class TestMiniFilter(unittest.TestCase): self._test(expr, {"title": "a(b)c"}, expected_result=True, test_name="quoted_parentheses_match") self._test(expr, {"title": "abc"}, expected_result=False, test_name="quoted_parentheses_no_match") - def test_escaped_ampersand_inside_quoted_regex(self): + def test_quoted_regex_ampersand(self): expr = r"description~='(?i)\bcats \& dogs\b'" parser = MiniFilter(expr) diff --git a/app/features/ytdlp/tests/test_ytdlp_module.py b/app/features/ytdlp/tests/test_ytdlp_module.py index 46e8daf4..a8c60e51 100644 --- a/app/features/ytdlp/tests/test_ytdlp_module.py +++ b/app/features/ytdlp/tests/test_ytdlp_module.py @@ -23,7 +23,7 @@ class TestArchiveProxy: assert p2.add("") is False @patch("app.features.ytdlp.archiver.Archiver.get_instance") - def test_contains_and_add_delegate_to_archiver(self, mock_get_instance) -> None: + def test_delegates_to_archiver(self, mock_get_instance) -> None: arch = MagicMock() mock_get_instance.return_value = arch @@ -47,7 +47,7 @@ class TestArchiveProxy: class TestYtDlpOptions: - def test_options_structure_and_no_suppresshelp(self) -> None: + def test_options_shape(self) -> None: opts = ytdlp_options() assert isinstance(opts, list) @@ -94,7 +94,7 @@ class TestYTDLP: return ytdlp @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL.__init__") - def test_init_patches_download_archive_param(self, mock_super_init) -> None: + def test_init_archive_param(self, mock_super_init) -> None: """Test that __init__ removes download_archive before calling super, then restores it.""" mock_super_init.return_value = None @@ -121,7 +121,7 @@ class TestYTDLP: assert ytdlp.archive._file == "/tmp/archive.txt" @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL.__init__") - def test_init_handles_no_download_archive(self, mock_super_init) -> None: + def test_init_no_archive(self, mock_super_init) -> None: """Test __init__ works correctly when download_archive is not in params.""" mock_super_init.return_value = None @@ -139,7 +139,7 @@ class TestYTDLP: assert not ytdlp.archive @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL.__init__") - def test_init_handles_none_params(self, mock_super_init) -> None: + def test_init_none_params(self, mock_super_init) -> None: """Test __init__ handles None params gracefully.""" mock_super_init.return_value = None @@ -150,7 +150,7 @@ class TestYTDLP: assert not ytdlp.archive @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL.__init__") - def test_init_patches_windows_popen_wait_once(self, mock_super_init) -> None: + def test_init_patches_wait(self, mock_super_init) -> None: mock_super_init.return_value = None class FakePopen: @@ -163,7 +163,7 @@ class TestYTDLP: assert getattr(FakePopen, "_ytptube_wait_patched", False) is True - def test_windows_wait_patch_uses_polling_for_blocking_wait(self) -> None: + def test_wait_patch_polls(self) -> None: calls: list[float | None] = [] class FakePopen: @@ -186,7 +186,7 @@ class TestYTDLP: assert result == 0 assert calls == [0.1, 0.1, 0.1] - def test_windows_wait_patch_preserves_explicit_timeout(self) -> None: + def test_wait_patch_timeout(self) -> None: calls: list[float | None] = [] class FakePopen: @@ -205,7 +205,7 @@ class TestYTDLP: assert calls == [5] @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files") - def test_delete_downloaded_files_skips_when_interrupted(self, mock_super_delete) -> None: + def test_delete_interrupted(self, mock_super_delete) -> None: """Test _delete_downloaded_files skips cleanup when _interrupted is True.""" with patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None): ytdlp = YTDLP(params={}) @@ -224,7 +224,7 @@ class TestYTDLP: assert result is None @patch("app.features.ytdlp.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files") - def test_delete_downloaded_files_calls_super_when_not_interrupted(self, mock_super_delete) -> None: + def test_delete_calls_super(self, mock_super_delete) -> None: """Test _delete_downloaded_files calls super when not interrupted.""" mock_super_delete.return_value = "cleanup_result" @@ -239,7 +239,7 @@ class TestYTDLP: # Should return super's result assert result == "cleanup_result" - def test_record_download_archive_does_nothing_without_download_archive_param(self) -> None: + def test_record_archive_missing(self) -> None: """Test record_download_archive returns early when download_archive is not set.""" ytdlp = self._create_ytdlp(params={}) ytdlp.archive = Mock() @@ -249,7 +249,7 @@ class TestYTDLP: # Should not interact with archive ytdlp.archive.add.assert_not_called() - def test_record_download_archive_adds_archive_id(self) -> None: + def test_record_archive_adds_id(self) -> None: """Test record_download_archive adds the archive ID.""" ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"}) ytdlp.write_debug = Mock() @@ -267,7 +267,7 @@ class TestYTDLP: ytdlp.archive.add.assert_called_once_with("youtube test123") ytdlp.write_debug.assert_called_with("Adding to archive: youtube test123") - def test_record_download_archive_handles_old_archive_ids(self) -> None: + def test_record_archive_old_ids(self) -> None: """Test record_download_archive adds _old_archive_ids when present.""" ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"}) ytdlp.write_debug = Mock() @@ -294,7 +294,7 @@ class TestYTDLP: # Should not add duplicate (youtube new123 appears only once) assert calls.count("youtube new123") == 1 - def test_record_download_archive_skips_empty_old_archive_ids(self) -> None: + def test_record_archive_empty_old_ids(self) -> None: """Test record_download_archive handles empty or invalid _old_archive_ids.""" ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"}) ytdlp.write_debug = Mock() @@ -320,7 +320,7 @@ class TestYTDLP: ytdlp.record_download_archive(info_dict) assert ytdlp.archive.add.call_count == 1 # Only main ID - def test_record_download_archive_returns_early_on_empty_archive_id(self) -> None: + def test_record_archive_empty_id(self) -> None: """Test record_download_archive returns early when _make_archive_id returns empty.""" ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"}) ytdlp.archive = Mock() @@ -331,7 +331,7 @@ class TestYTDLP: # Should not add anything ytdlp.archive.add.assert_not_called() - def test_prepare_outtmpl_resolves_custom_callable(self) -> None: + def test_outtmpl_callable(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) result = ytdlp.evaluate_outtmpl("%(ytp_random:8)s", {"title": "x"}) @@ -339,7 +339,7 @@ class TestYTDLP: assert len(result) == 8 assert result.isalnum() - def test_prepare_outtmpl_reuses_same_callable_value_per_download(self) -> None: + def test_outtmpl_reuses_value(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) result = ytdlp.evaluate_outtmpl("%(ytp_random:8)s/%(ytp_random:8)s", {"title": "x"}) @@ -347,7 +347,7 @@ class TestYTDLP: assert first == second - def test_prepare_outtmpl_does_not_reuse_callable_value_across_calls(self) -> None: + def test_outtmpl_new_value(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) first = ytdlp.evaluate_outtmpl("%(ytp_random:8)s", {"title": "x"}) @@ -355,7 +355,7 @@ class TestYTDLP: assert first != second - def test_prepare_filename_reuses_custom_value_for_sidecars_of_same_entry(self) -> None: + def test_prepare_filename_sidecars(self) -> None: ytdlp = YTDLP( params={ "outtmpl": { @@ -383,7 +383,7 @@ class TestYTDLP: assert default_base == subtitle_base assert default_base == infojson_base - def test_prepare_filename_resets_custom_value_for_different_info_dict_objects(self) -> None: + def test_prepare_filename_resets(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(ytp_random:8)s.%(ext)s"}}) first = ytdlp.prepare_filename({"id": "one", "title": "One", "ext": "mp4"}) @@ -399,7 +399,7 @@ class TestYTDLP: ("%(ytp_random:8)S", 8), ], ) - def test_prepare_outtmpl_preserves_ytdlp_suffix_formatting(self, template: str, expected: int) -> None: + def test_outtmpl_suffix(self, template: str, expected: int) -> None: ytdlp = YTDLP( params={ "outtmpl": {"default": "%(title)s"}, @@ -411,7 +411,7 @@ class TestYTDLP: assert len(result) == expected - def test_prepare_outtmpl_supports_digit_and_string_modes(self) -> None: + def test_outtmpl_modes(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) digits = ytdlp.evaluate_outtmpl("%(ytp_random:6:d)s", {"title": "x"}) @@ -420,13 +420,13 @@ class TestYTDLP: assert digits.isdigit() assert letters.isalpha() - def test_prepare_outtmpl_rejects_unknown_callable(self) -> None: + def test_outtmpl_unknown_callable(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) with pytest.raises(ValueError, match="Unsupported YTPTube output template callable"): ytdlp.prepare_outtmpl("%(ytp_unknown:8)s", {"title": "x"}) - def test_prepare_outtmpl_rejects_invalid_random_length(self) -> None: + def test_outtmpl_invalid_length(self) -> None: ytdlp = YTDLP(params={"outtmpl": {"default": "%(title)s"}}) with pytest.raises(ValueError, match="ytp_random length must be an integer"): @@ -434,7 +434,7 @@ class TestYTDLP: class TestOuttmpl: - def test_rewrite_outtmpl_uses_shared_cache_per_call_group(self) -> None: + def test_rewrite_outtmpl_cache(self) -> None: cache: dict[str, object] = {} template = "%(ytp_random:8)s/%(ytp_random:8)s.%(ext)s" diff --git a/app/features/ytdlp/tests/test_ytdlp_opts.py b/app/features/ytdlp/tests/test_ytdlp_opts.py index bb84391b..3bc15451 100644 --- a/app/features/ytdlp/tests/test_ytdlp_opts.py +++ b/app/features/ytdlp/tests/test_ytdlp_opts.py @@ -25,7 +25,7 @@ class TestYTDLPOpts: assert opts._item_cli == [] assert opts._preset_cli == "" - def test_get_instance_returns_reset_instance(self): + def test_get_instance(self): """Test that get_instance returns a reset YTDLPOpts instance.""" with patch("app.features.ytdlp.ytdlp_opts.Config"): opts = YTDLPOpts.get_instance() @@ -51,7 +51,7 @@ class TestYTDLPOpts: assert "--format best" in opts._item_cli mock_converter.assert_called_once_with(args="--format best", level=False) - def test_add_cli_with_invalid_args_raises_error(self): + def test_add_cli_invalid(self): """Test that invalid CLI arguments raise ValueError.""" with ( patch("app.features.ytdlp.ytdlp_opts.Config"), @@ -63,7 +63,7 @@ class TestYTDLPOpts: with pytest.raises(ValueError, match="Invalid command options for yt-dlp were given"): opts.add_cli("--invalid-arg", from_user=True) - def test_add_cli_with_empty_args_returns_self(self): + def test_add_cli_empty(self): """Test that empty or invalid args return self without processing.""" with patch("app.features.ytdlp.ytdlp_opts.Config"): opts = YTDLPOpts() @@ -95,7 +95,7 @@ class TestYTDLPOpts: assert opts._item_opts["format"] == "best" assert opts._item_opts["quality"] == "720p" - def test_add_with_user_config_filters_bad_options(self): + def test_add_filters_bad_options(self): """Test that user config filters out dangerous options.""" with patch("app.features.ytdlp.ytdlp_opts.Config"): opts = YTDLPOpts() @@ -211,7 +211,7 @@ class TestYTDLPOpts: assert opts._preset_opts["cookiefile"] == expected_path mock_preset.get_cookies_file.assert_called_once_with(config=mock_config_instance) - def test_preset_with_invalid_cli_raises_error(self): + def test_preset_invalid_cli(self): """Test that preset with invalid CLI raises ValueError.""" with ( patch("app.features.ytdlp.ytdlp_opts.Config"), @@ -289,7 +289,7 @@ class TestYTDLPOpts: expected_cli = "--quality 720p\n--format best" mock_converter.assert_called_once_with(args=expected_cli, level=True) - def test_get_all_handles_format_special_cases(self): + def test_get_all_format_cases(self): """Test get_all handles special format values correctly.""" with ( patch("app.features.ytdlp.ytdlp_opts.Config") as mock_config, @@ -325,7 +325,7 @@ class TestYTDLPOpts: result = opts.get_all(keep=True) assert result["format"] == "best" - def test_get_all_with_invalid_cli_raises_error(self): + def test_get_all_invalid_cli(self): """Test get_all raises error for invalid CLI arguments.""" with ( patch("app.features.ytdlp.ytdlp_opts.Config") as mock_config, @@ -578,7 +578,7 @@ class TestARGSMerger: assert "--output" in merger.args assert "--socket-timeout" in merger.args - def test_add_filters_complex_commented_extractor_args(self): + def test_add_complex_comments(self): """Test filtering of complex real-world commented extractor-args.""" from app.features.ytdlp.ytdlp_opts import ARGSMerger @@ -631,7 +631,7 @@ class TestARGSMerger: assert "--format" in merger.args assert "bestvideo[height<=1080]+bestaudio/best" in merger.args - def test_add_empty_string_returns_self(self): + def test_add_empty(self): """Test that adding empty string returns self without modifying args.""" from app.features.ytdlp.ytdlp_opts import ARGSMerger @@ -641,7 +641,7 @@ class TestARGSMerger: assert result is merger assert merger.args == [] - def test_add_short_string_returns_self(self): + def test_add_short(self): """Test that adding short string (len < 2) returns self without modifying args.""" from app.features.ytdlp.ytdlp_opts import ARGSMerger @@ -651,7 +651,7 @@ class TestARGSMerger: assert result is merger assert merger.args == [] - def test_add_non_string_returns_self(self): + def test_add_non_string(self): """Test that adding non-string returns self without modifying args.""" from app.features.ytdlp.ytdlp_opts import ARGSMerger @@ -769,7 +769,7 @@ class TestYTDLPCli: assert cli.item is item assert cli._config is mock_config_instance - def test_constructor_with_invalid_type_raises_error(self): + def test_constructor_invalid_item(self): """Test YTDLPCli constructor raises error with non-Item type.""" from app.features.ytdlp.ytdlp_opts import YTDLPCli diff --git a/app/features/ytdlp/tests/test_ytdlp_utils.py b/app/features/ytdlp/tests/test_ytdlp_utils.py index 863fa7ee..cf84e5fc 100644 --- a/app/features/ytdlp/tests/test_ytdlp_utils.py +++ b/app/features/ytdlp/tests/test_ytdlp_utils.py @@ -48,14 +48,14 @@ class TestLogWrapper: with pytest.raises(TypeError, match=r"Target must be a logging\.Logger instance or a callable"): lw.add_target(123) # type: ignore[arg-type] - def test_add_target_name_inference_and_custom(self) -> None: + def test_add_target_names(self) -> None: lw = LogWrapper() logger, _ = make_logger("one") # Name inferred from logger lw.add_target(logger) assert lw.targets[-1].name == "one" - assert lw.has_targets() is True + assert lw.has_targets() # Name inferred from callable def sink(level: int, msg: str, *args: Any, **kwargs: Any) -> None: # noqa: ARG001 @@ -129,7 +129,7 @@ class TestExtractYtdlpLogs: logs = ["INFO: Downloading", "ERROR: Failed", "WARNING: Deprecated"] filters = [re.compile(r"ERROR")] result = extract_ytdlp_logs(logs, filters) - assert len(result) >= 0 # Should filter based on patterns + assert result == ["ERROR: Failed"] def test_extract_ytdlp_logs_empty(self): """Test with empty logs.""" @@ -146,8 +146,8 @@ class TestYtdlpReject: yt_params = {} passed, message = ytdlp_reject(entry, yt_params) - assert isinstance(passed, bool) - assert isinstance(message, str) + assert passed is True + assert message == "" def test_ytdlp_reject_with_filters(self): """Test rejection with filters.""" @@ -158,8 +158,9 @@ class TestYtdlpReject: yt_params["daterange"].__contains__ = MagicMock(return_value=False) passed, message = ytdlp_reject(entry, yt_params) - assert isinstance(passed, bool) - assert isinstance(message, str) + assert passed is False + assert "20230101" in message + assert "not in range" in message class TestParseOuttmpl: @@ -256,7 +257,7 @@ class TestParseOuttmpl: assert result == "Test Channel/Best Videos/005 - Amazing Content [abc123xyz].mp4" - def test_parse_outtmpl_with_special_characters(self): + def test_parse_outtmpl_special_chars(self): """Test template parsing handles special characters in values.""" template = "%(title)s.%(ext)s" @@ -284,7 +285,7 @@ class TestParseOuttmpl: assert result == "My Playlist/Video Title.webm" - def test_parse_outtmpl_with_restrict_filename(self): + def test_parse_outtmpl_restrict(self): """Test template parsing with restrict_filename parameter.""" template = "%(uploader)s/%(title)s.%(ext)s" @@ -302,19 +303,19 @@ class TestParseOuttmpl: class TestGetThumbnail: - def test_returns_none_for_empty_list(self): + def test_empty_list(self): """Test that None is returned for an empty thumbnail list.""" assert get_thumbnail([]) is None - def test_returns_none_for_non_list(self): + def test_non_list(self): """Test that None is returned for non-list input.""" assert get_thumbnail(None) is None assert get_thumbnail("not a list") is None assert get_thumbnail({"not": "list"}) is None - def test_returns_highest_preference_thumbnail(self): + def test_thumbnail_preference(self): """Test that the thumbnail with highest preference is returned.""" thumbnails = [ @@ -326,7 +327,7 @@ class TestGetThumbnail: result = get_thumbnail(thumbnails) assert result == {"url": "high.jpg", "preference": 10, "width": 200, "height": 200} - def test_returns_highest_width_when_preference_equal(self): + def test_thumbnail_width(self): """Test that the thumbnail with highest width is returned when preference is equal.""" thumbnails = [ @@ -338,7 +339,7 @@ class TestGetThumbnail: result = get_thumbnail(thumbnails) assert result == {"url": "large.jpg", "preference": 1, "width": 200, "height": 200} - def test_handles_missing_attributes(self): + def test_missing_attrs(self): """Test that thumbnails with missing attributes are handled correctly.""" thumbnails = [ @@ -349,7 +350,7 @@ class TestGetThumbnail: result = get_thumbnail(thumbnails) assert result["url"] == "with_pref.jpg" - def test_returns_first_when_all_equal(self): + def test_all_equal(self): """Test that any thumbnail is returned when all attributes are equal.""" thumbnails = [ @@ -357,17 +358,15 @@ class TestGetThumbnail: {"url": "second.jpg"}, ] - result = get_thumbnail(thumbnails) - assert result is not None - assert result["url"] in ["first.jpg", "second.jpg"] + assert get_thumbnail(thumbnails) == {"url": "second.jpg"} class TestGetExtras: - def test_returns_empty_dict_for_none(self): + def test_none(self): """Test that empty dict is returned for None input.""" assert get_extras(None) == {} - def test_returns_empty_dict_for_non_dict(self): + def test_non_dict(self): """Test that empty dict is returned for non-dict input.""" assert get_extras("not a dict") == {} assert get_extras([]) == {} @@ -409,7 +408,7 @@ class TestGetExtras: assert result["playlist_uploader"] == "Playlist Owner" assert result["playlist_uploader_id"] == "owner123" - def test_handles_release_timestamp(self): + def test_release_timestamp(self): """Test handling of release_timestamp for upcoming content.""" entry = { @@ -421,7 +420,7 @@ class TestGetExtras: assert "release_in" in result assert result["release_in"] == "Fri, 13 Feb 2009 23:31:30 GMT" - def test_handles_upcoming_live_stream(self): + def test_upcoming_live(self): """Test handling of upcoming live stream.""" entry = { @@ -434,7 +433,7 @@ class TestGetExtras: assert result["is_live"] == 1234567890 assert "release_in" in result - def test_handles_premiere_flag(self): + def test_premiere_flag(self): """Test handling of is_premiere flag.""" entry = { @@ -481,7 +480,7 @@ class TestGetStaticYtdlp: _DATA.YTDLP_INFO_CLS = None - def test_get_static_ytdlp_returns_instance(self): + def test_instance(self): """Test that get_static_ytdlp returns a YTDLP instance.""" from app.features.ytdlp.ytdlp import YTDLP @@ -491,7 +490,7 @@ class TestGetStaticYtdlp: assert instance is not None assert isinstance(instance, YTDLP) - def test_get_static_ytdlp_returns_same_instance(self): + def test_get_static_ytdlp_same(self): """Test that get_static_ytdlp returns the same cached instance.""" instance1 = get_ytdlp() @@ -508,7 +507,7 @@ class TestGetStaticYtdlp: assert instance1 is not instance2 assert instance2 is not None - def test_get_static_ytdlp_has_correct_params(self): + def test_get_static_ytdlp_params(self): """Test that get_static_ytdlp initializes with correct parameters.""" instance = get_ytdlp() @@ -560,22 +559,22 @@ class TestArchiveFunctions: def test_archive_add_and_read(self): """Test adding and reading archive entries.""" - ids = ["id1", "id2", "id3"] + ids = ["youtube id1", "youtube id2", "youtube id3"] - # Add entries - just test it returns a boolean result = archive_add(self.archive_file, ids) - assert isinstance(result, bool) + assert result is True - # Read entries - just test it returns a list read_ids = archive_read(self.archive_file) - assert isinstance(read_ids, list) + assert set(read_ids) == set(ids) def test_archive_delete(self): """Test deleting archive entries.""" - # Delete some entries - just test it returns a boolean - delete_ids = ["id2"] + archive_add(self.archive_file, ["youtube id1", "youtube id2", "youtube id3"]) + + delete_ids = ["youtube id2"] result = archive_delete(self.archive_file, delete_ids) - assert isinstance(result, bool) + assert result is True + assert set(archive_read(self.archive_file)) == {"youtube id1", "youtube id3"} def test_archive_read_nonexistent(self): """Test reading from non-existent archive.""" diff --git a/app/tests/test_cache.py b/app/tests/test_cache.py index c03de8e8..bb7524c2 100644 --- a/app/tests/test_cache.py +++ b/app/tests/test_cache.py @@ -41,7 +41,7 @@ class TestCache: time.sleep(0.2) assert self.cache.get("temp_key") is None - def test_set_without_ttl(self): + def test_set_no_ttl(self): """Test setting values without TTL (permanent).""" self.cache.set("permanent_key", "permanent_value") assert self.cache.get("permanent_key") == "permanent_value" diff --git a/app/tests/test_condition_ignore.py b/app/tests/test_condition_ignore.py index f107b263..3991542f 100644 --- a/app/tests/test_condition_ignore.py +++ b/app/tests/test_condition_ignore.py @@ -18,7 +18,7 @@ def reset_conditions_singleton(): class TestConditionIgnoreMatching: @pytest.mark.asyncio - async def test_match_skips_condition_by_name_and_returns_next_match(self) -> None: + async def test_match_skip_name(self) -> None: first = SimpleNamespace(id=1, name="Primary", enabled=True, filter="duration > 0", priority=20) second = SimpleNamespace(id=2, name="Fallback", enabled=True, filter="duration > 0", priority=10) @@ -30,7 +30,7 @@ class TestConditionIgnoreMatching: assert matched is second @pytest.mark.asyncio - async def test_match_skips_condition_by_stringified_id_and_returns_next_match(self) -> None: + async def test_match_skip_id(self) -> None: first = SimpleNamespace(id=123, name="Primary", enabled=True, filter="duration > 0", priority=20) second = SimpleNamespace(id=124, name="Fallback", enabled=True, filter="duration > 0", priority=10) @@ -42,7 +42,7 @@ class TestConditionIgnoreMatching: assert matched is second @pytest.mark.asyncio - async def test_match_coerces_numeric_ignore_values_to_strings(self) -> None: + async def test_match_coerce_ids(self) -> None: first = SimpleNamespace(id=123, name="Primary", enabled=True, filter="duration > 0", priority=20) second = SimpleNamespace(id=124, name="Fallback", enabled=True, filter="duration > 0", priority=10) @@ -54,7 +54,7 @@ class TestConditionIgnoreMatching: assert matched is second @pytest.mark.asyncio - async def test_match_returns_none_when_ignore_all_wildcard_is_present(self) -> None: + async def test_match_wildcard(self) -> None: first = SimpleNamespace(id=1, name="Primary", enabled=True, filter="duration > 0", priority=20) service = Conditions.get_instance() @@ -67,7 +67,7 @@ class TestConditionIgnoreMatching: class TestConditionIgnorePropagation: @pytest.mark.asyncio - async def test_item_adder_passes_ignore_conditions_to_matcher(self) -> None: + async def test_add_passes_ignore(self) -> None: queue = Mock() queue.config = Mock(temp_path="/tmp", ignore_archived_items=False, ytdlp_debug=False) queue._notify = Mock() @@ -102,7 +102,7 @@ class TestConditionIgnorePropagation: assert result == {"status": "ok"} @pytest.mark.asyncio - async def test_item_adder_coerces_numeric_ignore_conditions_to_strings(self) -> None: + async def test_add_coerces_ignore(self) -> None: queue = Mock() queue.config = Mock(temp_path="/tmp", ignore_archived_items=False, ytdlp_debug=False) queue._notify = Mock() @@ -133,7 +133,7 @@ class TestConditionIgnorePropagation: matcher.match.assert_awaited_once_with(info=entry, ignore_conditions=["123", "Primary", "*"]) @pytest.mark.asyncio - async def test_process_playlist_preserves_only_parent_ignore_conditions(self) -> None: + async def test_playlist_keeps_parent_ignore(self) -> None: captured: dict[str, object] = {} class FakeItem: diff --git a/app/tests/test_datastore.py b/app/tests/test_datastore.py index dfb6bed3..d44120d0 100644 --- a/app/tests/test_datastore.py +++ b/app/tests/test_datastore.py @@ -218,7 +218,7 @@ class TestDataStore: assert ok is True @pytest.mark.asyncio - async def test_bulk_delete_by_status_drops_matching_cached_items(self) -> None: + async def test_bulk_delete_status_cache(self) -> None: connection = Mock() connection.bulk_delete_by_status = AsyncMock(return_value=2) store = DataStore(StoreType.HISTORY, connection) @@ -243,7 +243,7 @@ class TestDataStore: assert pending._id in store._dict @pytest.mark.asyncio - async def test_get_item_returns_none_when_no_kwargs(self) -> None: + async def test_get_item_no_filters(self) -> None: """Test that get_item returns None when no kwargs provided.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -319,7 +319,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_get_item_returns_none_when_no_match(self) -> None: + async def test_get_item_miss(self) -> None: """Test that get_item returns None when no attributes match.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -366,7 +366,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_get_item_returns_first_match(self) -> None: + async def test_get_item_first_match(self) -> None: """Test that get_item returns the first matching item when multiple match.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -689,7 +689,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_has_downloads_with_no_eligible_downloads(self) -> None: + async def test_has_downloads_ineligible(self) -> None: """Test has_downloads returns False when no downloads are eligible.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -708,7 +708,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_get_next_download_returns_none_when_empty(self) -> None: + async def test_next_download_empty(self) -> None: """Test get_next_download returns None when no eligible downloads.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -740,7 +740,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_update_store_item_removes_datetime_field(self) -> None: + async def test_update_item_drops_dt(self) -> None: """Test that _update_store_item removes datetime field before storage.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) @@ -760,7 +760,7 @@ class TestDataStore: await db.close() @pytest.mark.asyncio - async def test_update_store_item_removes_live_in_when_finished(self) -> None: + async def test_update_item_drops_live_in(self) -> None: """Test that _update_store_item removes live_in field when status is finished.""" store = await make_store_async(StoreType.QUEUE) conn = store._connection @@ -781,7 +781,7 @@ class TestDataStore: await store._connection.close() @pytest.mark.asyncio - async def test_update_store_item_keeps_live_in_when_not_finished(self) -> None: + async def test_update_item_keeps_live_in(self) -> None: """Test that _update_store_item keeps live_in field when status is not finished.""" store = await make_store_async(StoreType.QUEUE) conn = store._connection diff --git a/app/tests/test_download.py b/app/tests/test_download.py index 270f1296..53501413 100644 --- a/app/tests/test_download.py +++ b/app/tests/test_download.py @@ -327,7 +327,7 @@ class TestDownloadFlow: assert queue.items[0]["download_skipped"] is True assert queue.items[1]["download_skipped"] is True - def test_download_resets_sigint_handler_in_worker(self, monkeypatch: pytest.MonkeyPatch) -> None: + def test_download_resets_sigint(self, monkeypatch: pytest.MonkeyPatch) -> None: class Cfg: debug = False ytdlp_debug = False @@ -415,9 +415,7 @@ class TestDownloadFlow: assert ydl._interrupted is True ydl.to_screen.assert_called_once_with("[info] Interrupt received, exiting cleanly...") - def test_download_prefers_real_playlist_extras_over_placeholder_preinfo( - self, monkeypatch: pytest.MonkeyPatch - ) -> None: + def test_playlist_extras(self, monkeypatch: pytest.MonkeyPatch) -> None: class Cfg: debug = False ytdlp_debug = False @@ -606,9 +604,7 @@ class TestDownloadFlow: assert download.info.filename == "video.mp4", "Final filename should be set from status update" @pytest.mark.asyncio - async def test_live_cancelled_download_drains_final_status_updates( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: + async def test_live_cancel_drains_final(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: class Cfg: debug = False ytdlp_debug = False @@ -711,7 +707,7 @@ class TestDownloadFlow: assert download.info.filename == "live.mp4", "Finalized live filename should be preserved" @pytest.mark.asyncio - async def test_regular_cancelled_download_skips_live_drain_path(self, monkeypatch: pytest.MonkeyPatch) -> None: + async def test_regular_cancel_skips_drain(self, monkeypatch: pytest.MonkeyPatch) -> None: class Cfg: debug = False ytdlp_debug = False @@ -785,7 +781,7 @@ class TestDownloadSpawnPickling: def setup_method(self): EventBus._reset_singleton() - def test_spawn_pickling_ignores_local_event_listener(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + def test_spawn_pickling_ignores_listener(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: class Cfg: debug = False ytdlp_debug = False @@ -835,7 +831,7 @@ class TestDownloadSpawnPickling: class TestTempManager: - def test_create_temp_path_when_disabled(self) -> None: + def test_create_temp_path_disabled(self) -> None: info = make_item() logger = logging.getLogger("test") tm = TempManager(info, "/tmp", temp_disabled=True, temp_keep=False, logger=logger) @@ -844,7 +840,7 @@ class TestTempManager: assert result is None, "Should return None when temp_disabled is True" assert tm.temp_path is None, "temp_path should remain None when disabled" - def test_create_temp_path_when_no_temp_dir(self) -> None: + def test_create_temp_path_no_dir(self) -> None: info = make_item() logger = logging.getLogger("test") tm = TempManager(info, None, temp_disabled=False, temp_keep=False, logger=logger) @@ -874,7 +870,7 @@ class TestTempManager: path2 = tm2.create_temp_path() assert path1 == path2, "Same download ID should produce same temp path" - def test_delete_temp_when_disabled(self, tmp_path: Path) -> None: + def test_delete_temp_disabled(self, tmp_path: Path) -> None: info = make_item() logger = logging.getLogger("test") tm = TempManager(info, str(tmp_path), temp_disabled=True, temp_keep=False, logger=logger) @@ -884,7 +880,7 @@ class TestTempManager: tm.delete_temp() assert tm.temp_path.exists(), "Should not delete when temp_disabled is True" - def test_delete_temp_when_temp_keep_enabled(self, tmp_path: Path) -> None: + def test_delete_temp_keep(self, tmp_path: Path) -> None: info = make_item() logger = logging.getLogger("test") tm = TempManager(info, str(tmp_path), temp_disabled=False, temp_keep=True, logger=logger) @@ -894,12 +890,13 @@ class TestTempManager: tm.delete_temp() assert tm.temp_path.exists(), "Should not delete when temp_keep is True" - def test_delete_temp_when_no_temp_path(self) -> None: + def test_delete_temp_no_path(self) -> None: info = make_item() logger = logging.getLogger("test") tm = TempManager(info, "/tmp", temp_disabled=False, temp_keep=False, logger=logger) tm.delete_temp() + assert tm.temp_path is None, "temp_path should stay unset" def test_delete_temp_keeps_partial_download(self, tmp_path: Path) -> None: info = make_item() @@ -964,7 +961,7 @@ class TestProcessManager: assert pm.cancel_event.is_set() is False, "Should clear stale cancel events before starting" assert "download-test-id" == proc.name, "Process name should include download ID" - def test_started_returns_true_when_process_created(self) -> None: + def test_started(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) @@ -973,13 +970,13 @@ class TestProcessManager: pm.create_process(lambda: None) assert pm.started() is True, "Should return True after process created" - def test_running_returns_false_when_no_process(self) -> None: + def test_running_no_process(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) assert pm.running() is False, "Should return False when no process" - def test_is_cancelled_returns_false_by_default(self) -> None: + def test_is_cancelled_default(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) @@ -994,7 +991,7 @@ class TestProcessManager: result = pm.cancel() assert pm.is_cancelled() is True, "Should mark as cancelled" - def test_cancel_returns_false_when_not_started(self) -> None: + def test_cancel_not_started(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) @@ -1002,7 +999,7 @@ class TestProcessManager: assert result is False, "Should return False when process not started" assert pm.is_cancelled() is False, "Should not mark as cancelled when not started" - def test_kill_returns_false_when_not_running(self) -> None: + def test_kill_not_running(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) @@ -1025,7 +1022,7 @@ class TestProcessManager: mock_kill.assert_called_once_with(12345, signal.SIGUSR1) assert result is True, "Should return True when process killed successfully" - def test_kill_uses_live_cancel_event_and_longer_graceful_timeout(self) -> None: + def test_kill_live_uses_event(self) -> None: logger = logging.getLogger("test") pm_live = ProcessManager("test-id", is_live=True, logger=logger) pm_regular = ProcessManager("test-id", is_live=False, logger=logger) @@ -1065,7 +1062,7 @@ class TestProcessManager: mock_wait.assert_called_once_with(pm_regular.proc, 5) @pytest.mark.asyncio - async def test_close_returns_false_when_not_started(self) -> None: + async def test_close_not_started(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) @@ -1073,7 +1070,7 @@ class TestProcessManager: assert result is False, "Should return False when process not started" @pytest.mark.asyncio - async def test_close_returns_false_when_cancel_in_progress(self) -> None: + async def test_close_during_cancel(self) -> None: logger = logging.getLogger("test") pm = ProcessManager("test-id", is_live=False, logger=logger) pm.proc = Mock() @@ -1118,7 +1115,7 @@ class TestStatusTracker: assert st.final_update is False, "Should initialize final_update as False" @pytest.mark.asyncio - async def test_process_status_update_ignores_invalid_id(self, mock_config: dict) -> None: + async def test_status_ignores_bad_id(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = {"id": "wrong-id", "status": "downloading"} @@ -1126,7 +1123,7 @@ class TestStatusTracker: assert st.info.status != "downloading", "Should not update status for wrong ID" @pytest.mark.asyncio - async def test_process_status_update_ignores_short_status(self, mock_config: dict) -> None: + async def test_status_ignores_short(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = {"id": "test-id"} @@ -1141,7 +1138,7 @@ class TestStatusTracker: assert st.info.status == "downloading", "Should update info status" @pytest.mark.asyncio - async def test_process_status_update_sets_download_skipped(self, mock_config: dict) -> None: + async def test_status_sets_skipped(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = {"id": "test-id", "status": "downloading", "download_skipped": True} @@ -1157,7 +1154,7 @@ class TestStatusTracker: assert st.tmpfilename == "/tmp/file.part", "Should update tmpfilename" @pytest.mark.asyncio - async def test_process_status_update_calculates_percent(self, mock_config: dict) -> None: + async def test_status_sets_percent(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = { "id": "test-id", @@ -1172,7 +1169,7 @@ class TestStatusTracker: assert st.info.percent == 50.0, "Should calculate percent correctly" @pytest.mark.asyncio - async def test_process_status_update_uses_estimated_total(self, mock_config: dict) -> None: + async def test_status_uses_estimate(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = { "id": "test-id", @@ -1186,7 +1183,7 @@ class TestStatusTracker: assert st.info.percent == 30.0, "Should calculate percent from estimate" @pytest.mark.asyncio - async def test_process_status_update_handles_zero_division(self, mock_config: dict) -> None: + async def test_status_percent(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = { "id": "test-id", @@ -1199,7 +1196,7 @@ class TestStatusTracker: assert st.info.percent == 50.0, "Should calculate percent correctly with valid total" @pytest.mark.asyncio - async def test_process_status_update_sets_speed_and_eta(self, mock_config: dict) -> None: + async def test_status_sets_speed_eta(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) status = {"id": "test-id", "status": "downloading", "speed": 1024000, "eta": 60} @@ -1258,7 +1255,7 @@ class TestStatusTracker: assert st.final_update is True, "Should stop draining after final update" @pytest.mark.asyncio - async def test_drain_queue_handles_errors_gracefully(self, mock_config: dict) -> None: + async def test_drain_queue_skips_invalid(self, mock_config: dict) -> None: queue = DummyQueue() queue.put({"id": "test-id", "status": "downloading"}) queue.put(None) @@ -1267,8 +1264,9 @@ class TestStatusTracker: st = StatusTracker(**config) await st.drain_queue(max_iterations=5) + assert st.info.status == "downloading", "valid updates should still be processed" - def test_cancel_update_task_cancels_running_task(self, mock_config: dict) -> None: + def test_cancel_update_task(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) st.update_task = Mock() st.update_task.done = Mock(return_value=False) @@ -1277,10 +1275,11 @@ class TestStatusTracker: st.cancel_update_task() st.update_task.cancel.assert_called_once() - def test_cancel_update_task_handles_no_task(self, mock_config: dict) -> None: + def test_cancel_update_task_noop(self, mock_config: dict) -> None: st = StatusTracker(**mock_config) st.cancel_update_task() + assert st.update_task is None, "missing tasks should be ignored" def test_put_terminator_adds_to_queue(self, mock_config: dict) -> None: queue = DummyQueue() @@ -1316,7 +1315,7 @@ class TestQueueManager: assert status[item.info._id] == "ok", "Running cancel should still report success" @pytest.mark.asyncio - async def test_cancel_running_regular_item_closes_immediately(self) -> None: + async def test_cancel_regular_closes(self) -> None: queue_manager = object.__new__(DownloadQueue) queue_manager.queue = Mock() queue_manager.done = Mock() @@ -1338,7 +1337,7 @@ class TestQueueManager: assert status[item.info._id] == "ok", "Regular running cancel should still report success" @pytest.mark.asyncio - async def test_clear_flushes_history_deletes_before_returning(self) -> None: + async def test_clear_flushes_history(self) -> None: queue_manager = object.__new__(DownloadQueue) queue_manager.config = Mock(remove_files=False, download_path="/tmp") queue_manager._notify = Mock() @@ -1364,7 +1363,7 @@ class TestQueueManager: assert status[item.info._id] == "ok", "Clear should still report success after flushing deletes" @pytest.mark.asyncio - async def test_clear_bulk_uses_bulk_delete_and_aggregate_notification(self) -> None: + async def test_clear_bulk_notifies(self) -> None: queue_manager = object.__new__(DownloadQueue) queue_manager.config = Mock(remove_files=False, download_path="/tmp") queue_manager._notify = Mock() @@ -1394,7 +1393,7 @@ class TestQueueManager: assert queue_manager._notify.emit.call_args.kwargs["data"] == {"ids": ["done-id-1", "done-id-2"], "count": 2} @pytest.mark.asyncio - async def test_clear_by_status_uses_status_fetch_before_bulk_delete(self) -> None: + async def test_clear_status_fetches(self) -> None: queue_manager = object.__new__(DownloadQueue) queue_manager.config = Mock(remove_files=False, download_path="/tmp") queue_manager._notify = Mock() @@ -1414,7 +1413,7 @@ class TestQueueManager: assert queue_manager._notify.emit.call_args.kwargs["data"] == {"count": 1, "status": "finished"} @pytest.mark.asyncio - async def test_clear_by_status_with_file_removal_fetches_matching_items(self) -> None: + async def test_clear_status_files_fetch(self) -> None: queue_manager = object.__new__(DownloadQueue) queue_manager.config = Mock(remove_files=True, download_path="/tmp") queue_manager._notify = Mock() @@ -1438,7 +1437,7 @@ class TestQueueManager: class TestPoolManager: @pytest.mark.asyncio - async def test_cancelled_entry_with_final_file_stays_finished(self, monkeypatch: pytest.MonkeyPatch) -> None: + async def test_cancelled_file_stays_finished(self, monkeypatch: pytest.MonkeyPatch) -> None: emitted_events: list[str] = [] class EB: @@ -1483,9 +1482,7 @@ class TestPoolManager: done_store.put.assert_awaited_once_with(entry) @pytest.mark.asyncio - async def test_cancelled_regular_entry_with_final_file_stays_cancelled( - self, monkeypatch: pytest.MonkeyPatch - ) -> None: + async def test_cancelled_regular_file_stays(self, monkeypatch: pytest.MonkeyPatch) -> None: emitted_events: list[str] = [] class EB: diff --git a/app/tests/test_download_utils.py b/app/tests/test_download_utils.py index d349acfa..75cb530b 100644 --- a/app/tests/test_download_utils.py +++ b/app/tests/test_download_utils.py @@ -40,7 +40,7 @@ class TestPathUtilities: result = safe_relative_path(file, base, fallback) assert "video.mp4" == result, "Should use fallback path" - def test_safe_relative_path_no_fallback_returns_absolute(self) -> None: + def test_safe_rel_no_fallback(self) -> None: base = Path("/wrong/path") file = Path("/downloads/video.mp4") result = safe_relative_path(file, base) @@ -73,13 +73,13 @@ class TestPathUtilities: class TestProcessUtilities: - def test_wait_for_process_with_timeout_completes_immediately(self) -> None: + def test_wait_timeout_done(self) -> None: proc = Mock() proc.is_alive = Mock(return_value=False) result = wait_for_process_with_timeout(proc, timeout=1.0) assert result is True, "Should return True when process terminates immediately" - def test_wait_for_process_with_timeout_completes_after_delay(self) -> None: + def test_wait_timeout_delay(self) -> None: proc = Mock() call_count = [0] @@ -123,7 +123,7 @@ class TestConfigUtilities: result = parse_extractor_limit("youtube", default_limit=5, max_workers=10) assert 10 == result, "Should cap at max_workers" - def test_parse_extractor_limit_invalid_env_not_digit(self) -> None: + def test_parse_limit_nondigit(self) -> None: logger = logging.getLogger("test") with patch.dict(os.environ, {"YTP_MAX_WORKERS_FOR_YOUTUBE": "abc"}): result = parse_extractor_limit("youtube", default_limit=5, max_workers=10, logger=logger) @@ -138,11 +138,11 @@ class TestConfigUtilities: result = parse_extractor_limit("youtube", default_limit=5, max_workers=10) assert 5 == result, "Should use default when no env var set" - def test_parse_extractor_limit_respects_max_on_default(self) -> None: + def test_parse_limit_default_max(self) -> None: result = parse_extractor_limit("youtube", default_limit=15, max_workers=10) assert 10 == result, "Should cap default at max_workers" - def test_parse_extractor_limit_logs_warning_on_invalid(self) -> None: + def test_parse_limit_warns(self) -> None: logger = Mock() with patch.dict(os.environ, {"YTP_MAX_WORKERS_FOR_YOUTUBE": "invalid"}): parse_extractor_limit("youtube", default_limit=5, max_workers=10, logger=logger) @@ -195,7 +195,7 @@ class TestDataUtilities: assert "custom_field" not in result["info_dict"], "Should exclude custom field" assert "123" == result["info_dict"]["id"], "Should include id" - def test_create_debug_safe_dict_filters_none_and_functions(self) -> None: + def test_debug_safe_filters_none(self) -> None: data = { "status": "downloading", "info_dict": {"id": "123", "none_value": None, "lambda_value": lambda: None, "title": "Video"}, @@ -212,7 +212,7 @@ class TestDataUtilities: class TestStateUtilities: - def test_is_download_stale_terminal_status_finished(self) -> None: + def test_stale_finished(self) -> None: result = is_download_stale( started_time=int(time.time()) - 500, current_status="finished", is_running=False, auto_start=True ) @@ -224,19 +224,19 @@ class TestStateUtilities: ) assert result is False, "Error downloads are never stale" - def test_is_download_stale_terminal_status_cancelled(self) -> None: + def test_stale_cancelled(self) -> None: result = is_download_stale( started_time=int(time.time()) - 500, current_status="cancelled", is_running=False, auto_start=True ) assert result is False, "Cancelled downloads are never stale" - def test_is_download_stale_terminal_status_downloading(self) -> None: + def test_stale_downloading(self) -> None: result = is_download_stale( started_time=int(time.time()) - 500, current_status="downloading", is_running=False, auto_start=True ) assert result is False, "Downloading status is never stale" - def test_is_download_stale_terminal_status_postprocessing(self) -> None: + def test_stale_postprocessing(self) -> None: result = is_download_stale( started_time=int(time.time()) - 500, current_status="postprocessing", is_running=False, auto_start=True ) @@ -303,7 +303,7 @@ class TestTaskExceptionHandling: logger.error.assert_not_called() @pytest.mark.asyncio - async def test_handle_task_exception_ignores_successful(self) -> None: + async def test_task_exception_success(self) -> None: logger = Mock() async def successful_task(): diff --git a/app/tests/test_encoder.py b/app/tests/test_encoder.py index 163b72d6..68f74746 100644 --- a/app/tests/test_encoder.py +++ b/app/tests/test_encoder.py @@ -57,7 +57,7 @@ class TestEncoder: result = self.encoder.default(obj) assert result == {"name": "test", "value": 42} - def test_object_without_dict_fallback_to_default(self): + def test_object_default(self): """Test that objects without __dict__ fall back to default JSONEncoder.""" # This should raise TypeError since complex is not JSON serializable with pytest.raises(TypeError): diff --git a/app/tests/test_events.py b/app/tests/test_events.py index 6e756d1c..f820ea09 100644 --- a/app/tests/test_events.py +++ b/app/tests/test_events.py @@ -460,7 +460,7 @@ class TestEvent: assert f"handler3_{i}" in results @pytest.mark.asyncio - async def test_mixed_sync_async_handlers_execution_order(self): + async def test_mixed_handlers_order(self): """Test that mixed sync/async handlers execute properly without blocking.""" bus = EventBus() execution_times = [] @@ -511,7 +511,7 @@ class TestEvent: class TestEventListener: """Test the EventListener class.""" - def test_event_listener_creation_with_sync_callback(self): + def test_listener_sync_init(self): """Test creating EventListener with synchronous callback.""" def sync_callback(event, name, **kwargs): # noqa: ARG001 @@ -523,7 +523,7 @@ class TestEventListener: assert listener.call_back == sync_callback assert listener.is_coroutine is False - def test_event_listener_creation_with_async_callback(self): + def test_listener_async_init(self): """Test creating EventListener with asynchronous callback.""" async def async_callback(event, name, **kwargs): # noqa: ARG001 diff --git a/app/tests/test_history_routes.py b/app/tests/test_history_routes.py index 91c98016..9b7faf6b 100644 --- a/app/tests/test_history_routes.py +++ b/app/tests/test_history_routes.py @@ -52,7 +52,7 @@ def _make_download( @pytest.mark.asyncio -async def test_items_delete_uses_bulk_history_status_clear() -> None: +async def test_items_delete_status() -> None: request = _FakeRequest(payload={"type": StoreType.HISTORY.value, "status": "finished,skip", "remove_file": False}) queue = Mock() queue.clear_by_status = AsyncMock(return_value={"deleted": 12}) @@ -67,7 +67,7 @@ async def test_items_delete_uses_bulk_history_status_clear() -> None: @pytest.mark.asyncio -async def test_items_delete_uses_bulk_history_id_clear() -> None: +async def test_items_delete_ids() -> None: request = _FakeRequest(payload={"type": StoreType.HISTORY.value, "ids": ["a", "b"], "remove_file": False}) queue = Mock() queue.clear_bulk = AsyncMock(return_value={"deleted": 2}) @@ -82,7 +82,7 @@ async def test_items_delete_uses_bulk_history_id_clear() -> None: @pytest.mark.asyncio -async def test_item_rename_requires_new_name() -> None: +async def test_item_rename_needs_name() -> None: request = _FakeRequest(payload={}) request.match_info["id"] = "item-1" queue = SimpleNamespace( @@ -100,7 +100,7 @@ async def test_item_rename_requires_new_name() -> None: @pytest.mark.asyncio -async def test_item_rename_returns_not_found_when_item_missing() -> None: +async def test_item_rename_missing() -> None: request = _FakeRequest(payload={"new_name": "renamed.mp4"}) request.match_info["id"] = "missing" queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=None))) @@ -116,7 +116,7 @@ async def test_item_rename_returns_not_found_when_item_missing() -> None: @pytest.mark.asyncio -async def test_item_rename_requires_existing_downloaded_file() -> None: +async def test_item_rename_needs_file() -> None: request = _FakeRequest(payload={"new_name": "renamed.mp4"}) request.match_info["id"] = "item-1" item = _make_download(filename="video.mp4") @@ -135,7 +135,7 @@ async def test_item_rename_requires_existing_downloaded_file() -> None: @pytest.mark.asyncio -async def test_item_rename_renames_file_and_sidecars() -> None: +async def test_item_rename_sidecars() -> None: with temporary_test_dir("history-rename") as temp_dir: media = temp_dir / "video.mp4" subtitle = temp_dir / "video.en.srt" @@ -166,7 +166,7 @@ async def test_item_rename_renames_file_and_sidecars() -> None: @pytest.mark.asyncio -async def test_item_rename_returns_conflict_on_collision() -> None: +async def test_item_rename_conflict() -> None: with temporary_test_dir("history-rename-conflict") as temp_dir: media = temp_dir / "video.mp4" conflict = temp_dir / "renamed.mp4" diff --git a/app/tests/test_httpx_client.py b/app/tests/test_httpx_client.py index 729d8ccf..d9df6a9b 100644 --- a/app/tests/test_httpx_client.py +++ b/app/tests/test_httpx_client.py @@ -356,7 +356,7 @@ class TestAsyncClient: assert isinstance(client._transport, CFAsyncTransport) @pytest.mark.asyncio - async def test_async_client_cf_disabled(self): + async def test_async_client_custom_cf_off(self): """Test creating async client with CF disabled.""" async with async_client(enable_cf=False) as client: assert isinstance(client, httpx.AsyncClient) @@ -381,7 +381,7 @@ class TestAsyncClient: assert client._transport.base is custom @pytest.mark.asyncio - async def test_async_client_custom_transport_cf_disabled(self): + async def test_async_client_cf_disabled(self): """Test creating async client with custom transport and CF disabled.""" custom = httpx.AsyncHTTPTransport() async with async_client(enable_cf=False, transport=custom) as client: @@ -400,7 +400,7 @@ class TestSyncClient: assert isinstance(client._transport, CFTransport) client.close() - def test_sync_client_cf_disabled(self): + def test_sync_client_custom_cf_off(self): """Test creating sync client with CF disabled.""" client = sync_client(enable_cf=False) assert isinstance(client, httpx.Client) @@ -425,7 +425,7 @@ class TestSyncClient: assert client._transport.base is custom client.close() - def test_sync_client_custom_transport_cf_disabled(self): + def test_sync_client_cf_disabled(self): """Test creating sync client with custom transport and CF disabled.""" custom = httpx.HTTPTransport() client = sync_client(enable_cf=False, transport=custom) diff --git a/app/tests/test_itemdto.py b/app/tests/test_itemdto.py index 0358d57d..aef8ff14 100644 --- a/app/tests/test_itemdto.py +++ b/app/tests/test_itemdto.py @@ -43,7 +43,7 @@ class TestItemFormatAndBasics: assert item.cli == "--embed-metadata" @patch("app.features.presets.service.Presets.get_instance") - def test_format_raises_for_missing_url_and_invalid_preset(self, mock_presets_get): + def test_format_bad_input(self, mock_presets_get): # Missing url with pytest.raises(ValueError, match="url param is required"): Item.format({}) @@ -113,7 +113,7 @@ class TestItemDTO: @patch("app.library.ItemDTO.get_archive_id") @patch("app.library.ItemDTO.YTDLPOpts") @patch("app.library.ItemDTO.archive_read") - def test_post_init_does_not_infer_download_skipped_flag(self, mock_read, mock_opts, mock_get_id): + def test_post_init_keeps_skipped(self, mock_read, mock_opts, mock_get_id): mock_get_id.return_value = {"archive_id": "arch", "id": "arch", "ie_key": "YT"} mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value mock_opts.get_instance.return_value.add_cli.return_value = mock_opts.get_instance.return_value @@ -128,7 +128,7 @@ class TestItemDTO: assert dto.download_skipped is False @patch("app.library.ItemDTO.archive_read") - def test_serialize_triggers_archive_status_when_finished(self, mock_read): + def test_serialize_archives_finished(self, mock_read): # Given a finished item with archive info dto = ItemDTO(id="vid", title="t", url="u", folder="f") dto.archive_id = "arch" @@ -143,7 +143,7 @@ class TestItemDTO: assert key not in data @patch("app.library.ItemDTO.YTDLPOpts") - def test_serialize_does_not_recompute_download_skipped(self, mock_opts): + def test_serialize_keeps_skipped(self, mock_opts): mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value mock_opts.get_instance.return_value.add_cli.return_value = mock_opts.get_instance.return_value mock_opts.get_instance.return_value.get_all.return_value = {} @@ -299,7 +299,7 @@ class TestItemDTO: assert result is expected_sidecar assert dto.sidecar is expected_sidecar - def test_get_file_sidecar_returns_existing_when_no_file(self): + def test_sidecar_no_file(self): with patch.object(ItemDTO, "__post_init__", lambda _: None): dto = ItemDTO(id="sidecar-none", title="Title", url="u", folder="f") @@ -317,7 +317,7 @@ class TestItemDTO: assert result is existing assert dto.sidecar is existing - def test_get_preset_returns_preset_instance(self): + def test_get_preset_hit(self): """Test ItemDTO.get_preset returns the Preset instance.""" from app.features.presets.schemas import Preset @@ -335,7 +335,7 @@ class TestItemDTO: assert result is mock_preset assert result.name == "test-preset" - def test_get_preset_uses_default_when_no_preset_set(self): + def test_get_preset_default(self): """Test ItemDTO.get_preset uses 'default' when preset is empty.""" from app.features.presets.schemas import Preset @@ -352,7 +352,7 @@ class TestItemDTO: mock_presets.return_value.get.assert_called_once_with("default") assert result is mock_preset - def test_get_preset_returns_none_when_not_found(self): + def test_get_preset_miss(self): """Test ItemDTO.get_preset returns None when preset not found.""" with patch.object(ItemDTO, "__post_init__", lambda _: None): dto = ItemDTO(id="vid", title="t", url="u", folder="f", preset="nonexistent") @@ -376,7 +376,7 @@ class TestItemAddExtras: assert item.extras["key1"] == "value1" - def test_add_extras_when_extras_is_none(self): + def test_add_extras_none(self): """Test adding extras when extras is None.""" item = Item(url="https://example.com") item.extras = None diff --git a/app/tests/test_package_installer.py b/app/tests/test_package_installer.py index e07b736a..62f47ef4 100644 --- a/app/tests/test_package_installer.py +++ b/app/tests/test_package_installer.py @@ -38,7 +38,7 @@ class TestPackages: class TestPackageInstallerInit: - def test_init_with_explicit_path_adds_to_sys_path(self, tmp_path: Path) -> None: + def test_init_adds_sys_path(self, tmp_path: Path) -> None: p = tmp_path / "site" p.mkdir() @@ -58,7 +58,7 @@ class TestPackageInstallerInit: assert installer.user_site.exists() is True assert str(installer.user_site) in sys.path - def test_init_without_path_or_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + def test_init_no_path(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("YTP_CONFIG_PATH", raising=False) installer = PackageInstaller(pkg_path=None) # No user_site is set when no path or env provided @@ -206,7 +206,7 @@ class TestInstallCmd: class TestActionAndCheck: @patch.object(PackageInstaller, "_install_pkg") @patch.object(PackageInstaller, "_get_installed_version") - def test_action_skips_when_same_pinned(self, mock_get_installed, mock_install, tmp_path: Path) -> None: + def test_action_skip_pinned(self, mock_get_installed, mock_install, tmp_path: Path) -> None: inst = PackageInstaller(pkg_path=tmp_path) mock_get_installed.return_value = "1.2.3" # compare_versions normal equality should hold @@ -216,9 +216,7 @@ class TestActionAndCheck: @patch.object(PackageInstaller, "_install_pkg") @patch.object(PackageInstaller, "_get_installed_version") @patch.object(PackageInstaller, "_get_latest_version") - def test_action_upgrade_skip_when_latest( - self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path - ) -> None: + def test_action_skip_latest(self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path) -> None: inst = PackageInstaller(pkg_path=tmp_path) mock_get_installed.return_value = "2.0.0" mock_get_latest.return_value = "2.0.0" @@ -228,9 +226,7 @@ class TestActionAndCheck: @patch.object(PackageInstaller, "_install_pkg") @patch.object(PackageInstaller, "_get_installed_version") @patch.object(PackageInstaller, "_get_latest_version") - def test_action_upgrade_runs_when_newer_available( - self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path - ) -> None: + def test_action_upgrade_newer(self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path) -> None: inst = PackageInstaller(pkg_path=tmp_path) mock_get_installed.return_value = "1.0.0" mock_get_latest.return_value = "1.1.0" @@ -239,13 +235,13 @@ class TestActionAndCheck: @patch.object(PackageInstaller, "_install_pkg") @patch.object(PackageInstaller, "_get_installed_version") - def test_action_install_when_not_installed(self, mock_get_installed, mock_install, tmp_path: Path) -> None: + def test_action_install_missing(self, mock_get_installed, mock_install, tmp_path: Path) -> None: inst = PackageInstaller(pkg_path=tmp_path) mock_get_installed.return_value = None inst.action("pkg") mock_install.assert_called_once_with("pkg", version=None) - def test_check_with_no_packages_or_no_user_site(self, tmp_path: Path) -> None: + def test_check_no_packages_or_path(self, tmp_path: Path) -> None: # No packages inst = PackageInstaller(pkg_path=tmp_path) pkgs = Packages(env=None, file=None, upgrade=False) @@ -257,7 +253,7 @@ class TestActionAndCheck: inst2.check(pkgs2) @patch.object(PackageInstaller, "action") - def test_check_calls_action_and_handles_errors(self, mock_action, tmp_path: Path) -> None: + def test_check_runs_all(self, mock_action, tmp_path: Path) -> None: inst = PackageInstaller(pkg_path=tmp_path) pkgs = Packages(env="foo bar", file=None, upgrade=True) @@ -272,3 +268,5 @@ class TestActionAndCheck: # Should not raise inst.check(pkgs) assert mock_action.call_count == 2 + mock_action.assert_any_call("foo", upgrade=True) + mock_action.assert_any_call("bar", upgrade=True) diff --git a/app/tests/test_router.py b/app/tests/test_router.py index ba0913b6..f6ecf280 100644 --- a/app/tests/test_router.py +++ b/app/tests/test_router.py @@ -90,7 +90,7 @@ class TestAddRoute: assert r.method == "POST" assert r.path == "/api/create/" - def test_add_route_socket_without_alias(self) -> None: + def test_add_route_socket_no_alias(self) -> None: async def s(): return "s" @@ -109,7 +109,7 @@ class TestAddRoute: class TestGetters: - def test_get_routes_returns_copy_like_mapping(self) -> None: + def test_get_routes_copy(self) -> None: async def h(): return "x" diff --git a/app/tests/test_scheduler.py b/app/tests/test_scheduler.py index 08c47352..dfa5ef24 100644 --- a/app/tests/test_scheduler.py +++ b/app/tests/test_scheduler.py @@ -184,7 +184,7 @@ class TestScheduler: @pytest.mark.asyncio @patch("app.library.Scheduler.Cron", new=DummyCron) - async def test_attach_registers_shutdown_and_handles_schedule_add_event(self) -> None: + async def test_attach_registers_events(self) -> None: from aiohttp import web app = web.Application() @@ -224,7 +224,7 @@ class TestScheduler: assert kwargs["id"] == "evt-job" @patch("app.library.Scheduler.Cron") - def test_add_executes_function_when_cron_runs(self, cron_patch) -> None: + def test_add_executes_on_start(self, cron_patch) -> None: # Cron stub that auto-runs the function on creation when start=True class AutoRunCron(DummyCron): def __init__( diff --git a/app/tests/test_services.py b/app/tests/test_services.py index 10aa66ad..c2a988cc 100644 --- a/app/tests/test_services.py +++ b/app/tests/test_services.py @@ -75,7 +75,7 @@ class TestServices: assert services.get("service3") == "value3" assert len(services.get_all()) == 3 - def test_get_all_returns_copy(self): + def test_get_all_copy(self): """Test that get_all returns a copy, not the original dict.""" services = Services() services.add("test", "value") diff --git a/app/tests/test_sqlite_store.py b/app/tests/test_sqlite_store.py index 56326fa1..6039ae3f 100644 --- a/app/tests/test_sqlite_store.py +++ b/app/tests/test_sqlite_store.py @@ -32,7 +32,7 @@ def make_item(idx: int, *, status: str = "finished", cli: str = "", download_ski @pytest.mark.asyncio -async def test_sessionmaker_returns_valid_sessionmaker() -> None: +async def test_sessionmaker_ready() -> None: """Test that sessionmaker() returns a working async_sessionmaker.""" SqliteStore._reset_singleton() store = SqliteStore.get_instance(db_path=make_in_memory_db_path("sessionmaker")) @@ -120,7 +120,7 @@ async def test_enqueue_upsert_and_fetch_saved(): @pytest.mark.asyncio -async def test_enqueue_upsert_persists_download_skipped_flag(): +async def test_enqueue_upsert_skipped(): store = await make_store() item = make_item(2, download_skipped=True) @@ -237,7 +237,7 @@ async def test_enqueue_delete_removes_row(): @pytest.mark.asyncio -async def test_enqueue_bulk_delete_returns_count_and_bulk_path(): +async def test_enqueue_bulk_delete(): store = await make_store() items = [make_item(i) for i in range(3)] for itm in items: @@ -258,7 +258,7 @@ async def test_enqueue_bulk_delete_returns_count_and_bulk_path(): @pytest.mark.asyncio -async def test_get_many_by_ids_and_status_and_bulk_delete_by_status(): +async def test_ids_status_bulk_delete(): store = await make_store() finished = [make_item(i, status="finished") for i in range(2)] pending = [make_item(i + 10, status="pending") for i in range(2)] @@ -296,7 +296,7 @@ async def test_get_many_by_ids_and_status_and_bulk_delete_by_status(): @pytest.mark.asyncio -async def test_paginate_out_of_range_returns_last_page(): +async def test_paginate_last_page(): store = await make_store() for i in range(7): await store.enqueue_upsert("history", make_item(i)) @@ -311,7 +311,7 @@ async def test_paginate_out_of_range_returns_last_page(): @pytest.mark.asyncio -async def test_get_item_returns_none_without_kwargs(): +async def test_get_item_no_filters(): store = await make_store() await store.enqueue_upsert("queue", make_item(1)) await store.flush() @@ -387,7 +387,7 @@ async def test_exists_and_get_by_key_and_url(): @pytest.mark.asyncio -async def test_exists_and_get_raise_without_key_or_url(): +async def test_exists_and_get_require_lookup(): store = await make_store() with pytest.raises(KeyError): await store.exists("queue") diff --git a/app/tests/test_static_routes.py b/app/tests/test_static_routes.py index f17dae09..8b40a5c6 100644 --- a/app/tests/test_static_routes.py +++ b/app/tests/test_static_routes.py @@ -34,7 +34,7 @@ def _configure_static_root(static_root: Path) -> None: class TestServeStaticFile: @pytest.mark.asyncio - async def test_nested_document_route_falls_back_to_spa_shell(self, tmp_path: Path) -> None: + async def test_nested_doc_falls_back(self, tmp_path: Path) -> None: config = Config.get_instance() index_file = tmp_path / "index.html" index_file.write_text("root shell", encoding="utf-8") @@ -46,7 +46,7 @@ class TestServeStaticFile: assert response._path == index_file @pytest.mark.asyncio - async def test_generated_nested_index_is_not_preferred_over_root_shell(self, tmp_path: Path) -> None: + async def test_nested_index_uses_root(self, tmp_path: Path) -> None: config = Config.get_instance() root_index = tmp_path / "index.html" nested_index = tmp_path / "docs" / "readme" / "index.html" @@ -82,7 +82,7 @@ class TestServeStaticFile: assert b"missing.js" in body @pytest.mark.asyncio - async def test_missing_asset_with_unknown_suffix_does_not_fall_back(self, tmp_path: Path) -> None: + async def test_missing_unknown_no_fallback(self, tmp_path: Path) -> None: config = Config.get_instance() (tmp_path / "index.html").write_text("root shell", encoding="utf-8") assets_dir = tmp_path / "assets" @@ -103,7 +103,7 @@ class TestServeStaticFile: assert b"missing.abcd123" in body @pytest.mark.asyncio - async def test_symlink_outside_static_root_does_not_resolve(self, tmp_path: Path) -> None: + async def test_symlink_outside_rejected(self, tmp_path: Path) -> None: config = Config.get_instance() (tmp_path / "index.html").write_text("root shell", encoding="utf-8") outside_dir = tmp_path.parent / "outside-static-root" @@ -144,7 +144,7 @@ class TestServeStaticFile: assert b"/api/missing" in body @pytest.mark.asyncio - async def test_dotted_browser_path_returns_not_found(self, tmp_path: Path) -> None: + async def test_dotted_browser_path_404(self, tmp_path: Path) -> None: config = Config.get_instance() (tmp_path / "index.html").write_text("root shell", encoding="utf-8") _configure_static_root(tmp_path) @@ -157,7 +157,7 @@ class TestServeStaticFile: assert response.status == web.HTTPNotFound.status_code assert b"/browser/foo/bar.txt" in body - def test_registers_only_root_and_catch_all_routes(self, tmp_path: Path) -> None: + def test_registers_root_routes(self, tmp_path: Path) -> None: config = Config.get_instance() static_root = tmp_path / "ui-exported" static_root.mkdir() diff --git a/app/tests/test_system_routes.py b/app/tests/test_system_routes.py index f578e248..eefbfda5 100644 --- a/app/tests/test_system_routes.py +++ b/app/tests/test_system_routes.py @@ -121,7 +121,7 @@ class TestSystemLimitsEndpoint: Config._reset_singleton() @pytest.mark.asyncio - async def test_system_limits_returns_user_facing_limits(self): + async def test_system_limits_public(self): config = Config.get_instance() config.max_workers = 10 config.max_workers_per_extractor = 3 @@ -188,7 +188,7 @@ class TestSystemLimitsEndpoint: "available": 3, } - def test_config_reads_prevent_live_premiere_boolean_env(self): + def test_config_reads_live_premiere(self): with patch.dict("os.environ", {"YTP_PREVENT_LIVE_PREMIERE": "false"}, clear=False): Config._reset_singleton() config = Config.get_instance() diff --git a/app/tests/test_terminal_session_manager.py b/app/tests/test_terminal_session_manager.py index 1407683d..236f369a 100644 --- a/app/tests/test_terminal_session_manager.py +++ b/app/tests/test_terminal_session_manager.py @@ -166,7 +166,7 @@ class TestTerminalSessionRoutes: assert manager._cleanup_job_id == f"{TerminalSessionManager.__name__}.{TerminalSessionManager.cleanup.__name__}" @pytest.mark.asyncio - async def test_start_returns_session_metadata_and_active_conflict( + async def test_start_conflict_meta( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -206,7 +206,7 @@ class TestTerminalSessionRoutes: await task @pytest.mark.asyncio - async def test_stream_endpoint_replays_persisted_events_and_resume_cursor( + async def test_stream_replays_resume( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -256,7 +256,7 @@ class TestTerminalSessionRoutes: assert "id: 3" in resumed_payload @pytest.mark.asyncio - async def test_completed_session_expires_after_drain_window( + async def test_completed_expires( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -293,7 +293,7 @@ class TestTerminalSessionRoutes: assert not (manager.root_path / session_id).exists() @pytest.mark.asyncio - async def test_list_sessions_returns_recent_completed_session_until_retention_expires( + async def test_list_keeps_recent_done( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -341,9 +341,7 @@ class TestTerminalSessionRoutes: assert not (manager.root_path / session_id).exists() @pytest.mark.asyncio - async def test_list_sessions_orders_newest_first_and_skips_expired_items( - self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder] - ) -> None: + async def test_list_orders_newest(self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder]) -> None: config, manager, encoder = terminal_setup await manager.initialize() @@ -411,7 +409,7 @@ class TestTerminalSessionRoutes: assert not (manager.root_path / expired_id).exists() @pytest.mark.asyncio - async def test_shutdown_interrupts_active_session_and_clears_active_marker( + async def test_shutdown_clears_active( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -451,7 +449,7 @@ class TestTerminalSessionRoutes: assert -15 == transcript[-1]["data"]["exitcode"] @pytest.mark.asyncio - async def test_stream_endpoint_emits_keepalive_for_silent_active_session( + async def test_stream_keepalive( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -489,7 +487,7 @@ class TestTerminalSessionRoutes: assert 'data: {"exitcode": 0}' in stream_payload @pytest.mark.asyncio - async def test_cancel_endpoint_interrupts_active_session( + async def test_cancel_active( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -535,7 +533,7 @@ class TestTerminalSessionRoutes: assert -15 == transcript[-1]["data"]["exitcode"] @pytest.mark.asyncio - async def test_cancel_endpoint_returns_conflict_for_inactive_session( + async def test_cancel_inactive_conflict( self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch ) -> None: config, manager, encoder = terminal_setup @@ -564,9 +562,7 @@ class TestTerminalSessionRoutes: assert b"not active" in cancel_response.body.lower() @pytest.mark.asyncio - async def test_cancel_endpoint_returns_not_found_for_unknown_session( - self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder] - ) -> None: + async def test_cancel_unknown(self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder]) -> None: config, manager, encoder = terminal_setup await manager.initialize() diff --git a/app/tests/test_test_helpers.py b/app/tests/test_test_helpers.py index 4f52e55e..947a47f8 100644 --- a/app/tests/test_test_helpers.py +++ b/app/tests/test_test_helpers.py @@ -10,14 +10,14 @@ from app.tests.helpers import ( ) -def test_make_test_disk_path_uses_test_run_root() -> None: +def test_disk_path_root() -> None: path = make_test_disk_path("artifacts", "example.txt") assert path.parent.exists() assert path.is_relative_to(get_test_run_root()) -def test_make_test_temp_dir_creates_directory() -> None: +def test_temp_dir_created() -> None: path = make_test_temp_dir("helpers") assert path.exists() @@ -25,7 +25,7 @@ def test_make_test_temp_dir_creates_directory() -> None: assert path.is_relative_to(get_test_run_root()) -def test_tmp_path_runs_under_custom_temp_root(tmp_path: Path) -> None: +def test_tmp_path_root(tmp_path: Path) -> None: expected_root = get_test_run_root() / "pytest" assert tmp_path.is_relative_to(expected_root) diff --git a/app/tests/test_update_checker.py b/app/tests/test_update_checker.py index 25687e68..4cb23699 100644 --- a/app/tests/test_update_checker.py +++ b/app/tests/test_update_checker.py @@ -21,7 +21,7 @@ class TestUpdateChecker: EventBus._reset_singleton() Cache._reset_singleton() - def test_attach_schedules_check_when_enabled(self): + def test_attach_enabled(self): """Test that attach schedules update check when config.check_for_updates is True.""" import asyncio @@ -50,7 +50,7 @@ class TestUpdateChecker: scheduler.remove(checker._job_id) loop.close() - def test_attach_skips_scheduling_when_disabled(self): + def test_attach_disabled(self): """Test that attach skips scheduling when config.check_for_updates is False.""" from app.library.config import Config from app.library.UpdateChecker import UpdateChecker @@ -111,7 +111,7 @@ class TestUpdateChecker: loop.close() @pytest.mark.asyncio - async def test_check_for_updates_skips_when_disabled(self): + async def test_check_for_updates_disabled(self): """Test that check_for_updates skips when config.check_for_updates is False.""" from app.library.config import Config from app.library.UpdateChecker import UpdateChecker @@ -255,7 +255,7 @@ class TestUpdateChecker: @pytest.mark.asyncio @patch("app.library.UpdateChecker.get_async_client") - async def test_check_ytdlp_version_handles_http_error(self, mock_client): + async def test_check_ytdlp_version_http_error(self, mock_client): """Test that yt-dlp check handles HTTP errors gracefully.""" from app.library.config import Config from app.library.UpdateChecker import UpdateChecker diff --git a/app/tests/test_utils.py b/app/tests/test_utils.py index e2375067..ff65a372 100644 --- a/app/tests/test_utils.py +++ b/app/tests/test_utils.py @@ -1,5 +1,6 @@ import asyncio import copy +import importlib import re import shutil import uuid @@ -30,7 +31,6 @@ from app.library.Utils import ( is_private_address, list_folders, load_cookies, - load_modules, merge_dict, move_file, parse_tags, @@ -48,7 +48,7 @@ from app.tests.helpers import make_test_temp_dir, temporary_test_dir class TestTimedLruCache: """Test the timed_lru_cache decorator.""" - def test_timed_lru_cache_basic_functionality(self): + def test_basic(self): """Test that timed_lru_cache caches function results.""" call_count = 0 @@ -332,39 +332,39 @@ class TestCalcDownloadPath: result = calc_download_path(str(self.base_path), None, create_path=False) assert result == str(self.base_path), "Should return base path when folder is None" - def test_calc_download_path_removes_leading_slash(self): + def test_calc_path_strips_leading_slash(self): """Test that leading slash is removed from folder.""" folder = "/test_folder" result = calc_download_path(str(self.base_path), folder, create_path=False) expected = str(self.base_path / "test_folder") assert result == expected, "Should remove leading slash from folder" - def test_calc_download_path_path_traversal_dotdot(self): + def test_calc_path_dotdot(self): """Test path traversal prevention with .. sequences.""" folder = "../outside" with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_path_traversal_nested_dotdot(self): + def test_calc_path_nested_dotdot(self): """Test path traversal prevention with nested .. sequences.""" folder = "safe/../../outside" with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_path_traversal_multiple_dotdot(self): + def test_calc_path_multi_dotdot(self): """Test path traversal prevention with multiple .. sequences.""" folder = "../../../etc/passwd" with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_path_traversal_absolute_path(self): + def test_calc_path_absolute(self): """Test that absolute paths are made safe by removing leading slash.""" folder = "/etc/passwd" result = calc_download_path(str(self.base_path), folder, create_path=False) expected = str(self.base_path / "etc/passwd") assert result == expected, "Should remove leading slash and treat as relative path" - def test_calc_download_path_path_traversal_absolute_with_dotdot(self): + def test_calc_path_absolute_dotdot(self): """Test path traversal with absolute path containing .. sequences.""" folder = "/../../../etc/passwd" with pytest.raises(Exception, match="must resolve inside the base download folder"): @@ -376,7 +376,7 @@ class TestCalcDownloadPath: with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_path_traversal_url_encoded(self): + def test_calc_path_url_encoded(self): """Test path traversal prevention with URL encoded sequences.""" folder = "safe%2F..%2F..%2Funsafe" # safe/../unsafe encoded # This should be handled at a higher level, but let's test it anyway @@ -449,7 +449,7 @@ class TestCalcDownloadPath: with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_carriage_return_attack(self): + def test_calc_path_carriage_return(self): """Test that carriage returns in path traversal are prevented.""" folder = "folder\r../../../etc/passwd" with pytest.raises(Exception, match="must resolve inside the base download folder"): @@ -479,7 +479,7 @@ class TestCalcDownloadPath: with pytest.raises(Exception, match="must resolve inside the base download folder"): calc_download_path(str(self.base_path), folder, create_path=False) - def test_calc_download_path_url_encoded_traversal_safe(self): + def test_calc_path_url_encoded_safe(self): """Test that URL encoded path traversal is treated as literal (safe).""" folder = "folder..%2F..%2F..%2Fetc%2Fpasswd" # ..%2F = ../ encoded result = calc_download_path(str(self.base_path), folder, create_path=False) @@ -494,7 +494,7 @@ class TestCalcDownloadPath: expected = str(self.base_path / folder) assert result == expected, "Backslashes should be treated as literal characters on Unix" - def test_calc_download_path_mixed_separators_attack(self): + def test_calc_path_mixed_separators(self): """Test path traversal with mixed separators.""" folder = "folder/../../../etc/passwd" with pytest.raises(Exception, match="must resolve inside the base download folder"): @@ -520,7 +520,7 @@ class TestCalcDownloadPath: # Clean up shutil.rmtree(sibling_dir, ignore_errors=True) - def test_calc_download_path_symlink_attack_outside(self): + def test_calc_path_symlink_outside(self): """Test that symlinks pointing outside base directory are blocked.""" # Create a symlink pointing outside the base directory outside_dir = Path(self.temp_dir).parent / "outside_target" @@ -539,7 +539,7 @@ class TestCalcDownloadPath: symlink_path.unlink() shutil.rmtree(outside_dir, ignore_errors=True) - def test_calc_download_path_symlink_attack_with_traversal(self): + def test_calc_path_symlink_traversal(self): """Test symlink combined with path traversal.""" # Create a directory outside base outside_dir = Path(self.temp_dir).parent / "target_dir" @@ -563,7 +563,7 @@ class TestCalcDownloadPath: shutil.rmtree(safe_dir, ignore_errors=True) shutil.rmtree(outside_dir, ignore_errors=True) - def test_calc_download_path_symlink_safe_internal(self): + def test_calc_path_symlink_internal(self): """Test that symlinks pointing inside base directory are allowed.""" # Create target directory inside base target_dir = self.base_path / "target" @@ -602,7 +602,7 @@ class TestCalcDownloadPath: expected = str(self.base_path / deep_path) assert result == expected, "Should handle deeply nested paths" - def test_calc_download_path_directory_with_spaces(self): + def test_calc_path_spaces(self): """Test paths with multiple spaces and special spacing.""" folder = "folder with multiple spaces" result = calc_download_path(str(self.base_path), folder, create_path=True) @@ -697,7 +697,7 @@ class TestMergeDict: assert "__builtins__" not in result, "__builtins__ should be filtered out" assert result["normal"] == "value", "Normal values should be preserved" - def test_merge_dict_blocks_multiple_dunder_pollution(self): + def test_merge_dict_blocks_dunders(self): """Test that multiple dangerous dunder attributes are blocked.""" source = { "__class__": "malicious", @@ -863,7 +863,7 @@ class TestMergeDict: assert len(result["data"]) == 5000 assert result["data"] == large_list - def test_merge_dict_list_merging_with_size_limits(self): + def test_merge_dict_list_limits(self): """Test list merging respects size limits.""" source = {"items": list(range(3000))} destination = {"items": list(range(2000, 5000))} # 3000 items @@ -921,7 +921,7 @@ class TestMergeDict: result = merge_dict(source, {}, max_depth=10000, max_list_size=1000000) assert result["a"]["b"]["c"]["data"] == "nested" - def test_merge_dict_limits_with_circular_reference_protection(self): + def test_merge_dict_circular_guard(self): """Test that limits work together with circular reference protection.""" source = {"data": {}} source["data"]["circular"] = source # Create circular reference @@ -1088,9 +1088,7 @@ class TestValidateUuid: def test_wrong_version(self): """Test UUID4 against version 1 check.""" test_uuid = str(uuid.uuid4()) - # Version check is not strict in this function - it may return True for any valid UUID - result = validate_uuid(test_uuid, 1) - assert isinstance(result, bool) # Just check it returns a boolean + assert validate_uuid(test_uuid, 1) is True class TestStripNewline: @@ -1222,14 +1220,13 @@ class TestValidateUrl: def test_validate_url_basic(self): """Test basic URL validation functionality.""" - # Test without actual validation due to missing yarl dependency - # Just check the function exists and handles the missing dependency gracefully - try: - result = validate_url("https://example.com") - assert isinstance(result, bool) - except ModuleNotFoundError: - # Expected when yarl is not installed - assert True + if importlib.util.find_spec("yarl") is None: + with pytest.raises(ModuleNotFoundError): + validate_url("https://example.com") + return + + result = validate_url("https://example.com", allow_internal=True) + assert result is True class TestGetFileSidecar: @@ -1300,32 +1297,36 @@ class TestArgConverter: def test_arg_converter_basic(self): """Test basic arg_converter functionality.""" - try: - result = arg_converter("--quiet --match-filters 'duration<2min' --download-archive archive.txt") - assert isinstance(result, dict) - assert result.get("quiet") is True, "quiet should be True" - assert result.get("download_archive") == "archive.txt" - assert "match_filter" in result, "match_filters should be in result" - except (ModuleNotFoundError, AttributeError, ImportError): - # Expected when yt_dlp is not available or differs - assert True + if importlib.util.find_spec("yt_dlp") is None: + with pytest.raises(ModuleNotFoundError): + arg_converter("--quiet --match-filters 'duration<2min' --download-archive archive.txt") + return + + result = arg_converter("--quiet --match-filters 'duration<2min' --download-archive archive.txt") + assert isinstance(result, dict) + assert result.get("quiet") is True, "quiet should be True" + assert result.get("download_archive") == "archive.txt" + assert "match_filter" in result, "match_filters should be in result" def test_arg_converter_empty_args(self): """Test arg_converter with empty args.""" - try: - result = arg_converter("") - assert isinstance(result, dict) - except (ModuleNotFoundError, AttributeError, ImportError): - assert True + if importlib.util.find_spec("yt_dlp") is None: + with pytest.raises(ModuleNotFoundError): + arg_converter("") + return + + result = arg_converter("") + assert isinstance(result, dict) def test_arg_converter_replace_in_metadata(self): """Test arg_converter handles replace-in-metadata without assertions.""" - try: - result = arg_converter("--replace-in-metadata title foo bar") - except (ModuleNotFoundError, AttributeError, ImportError): - assert True + if importlib.util.find_spec("yt_dlp") is None: + with pytest.raises(ModuleNotFoundError): + arg_converter("--replace-in-metadata title foo bar") return + result = arg_converter("--replace-in-metadata title foo bar") + postprocessors = result.get("postprocessors", []) assert postprocessors, "Expected metadata parser postprocessor to be present" @@ -1584,11 +1585,10 @@ class TestLoadCookies: try: valid, jar = load_cookies(str(self.cookie_file)) - assert isinstance(valid, bool) - assert jar is not None or not valid + assert valid is False + assert jar is not None except ValueError: - # Expected for invalid cookie files - assert True + return class TestStrToDt: @@ -1596,20 +1596,23 @@ class TestStrToDt: def test_str_to_dt_basic(self): """Test basic string to datetime conversion.""" - try: - result = str_to_dt("2023-01-02 12:00:00 UTC") - assert isinstance(result, datetime) - except ModuleNotFoundError: - # Expected when dateparser is not available - assert True + if importlib.util.find_spec("dateparser") is None: + with pytest.raises(ModuleNotFoundError): + str_to_dt("2023-01-02 12:00:00 UTC") + return + + result = str_to_dt("2023-01-02 12:00:00 UTC") + assert isinstance(result, datetime) def test_str_to_dt_relative(self): """Test relative time string.""" - try: - result = str_to_dt("1 hour ago") - assert isinstance(result, datetime) - except (ModuleNotFoundError, ValueError): - assert True + if importlib.util.find_spec("dateparser") is None: + with pytest.raises(ModuleNotFoundError): + str_to_dt("1 hour ago") + return + + result = str_to_dt("1 hour ago") + assert isinstance(result, datetime) class TestInitClass: @@ -1633,37 +1636,6 @@ class TestInitClass: assert result.unused == "default" # Should use default -class TestLoadModules: - """Test the load_modules function.""" - - def setup_method(self): - """Set up test module structure.""" - self.temp_dir = str(make_test_temp_dir("load-modules")) - self.root_path = Path(self.temp_dir) - self.module_dir = self.root_path / "test_modules" - self.module_dir.mkdir() - - # Create test module files - (self.module_dir / "__init__.py").write_text("") - (self.module_dir / "test_module.py").write_text("# Test module") - - def teardown_method(self): - """Clean up after tests.""" - import shutil - - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def test_load_modules_basic(self): - """Test basic module loading.""" - try: - load_modules(self.root_path, self.module_dir) - # Should not raise an exception - assert True - except Exception: - # Module loading might fail in test environment - assert True - - class TestGetChannelImages: """Test the get_channel_images function.""" @@ -1711,7 +1683,7 @@ class TestGetChannelImages: assert "icon" in result assert result["icon"] == "http://example.com/icon.jpg" - def test_get_channel_images_landscape_from_banner_uncropped(self): + def test_channel_images_landscape_banner(self): """Test extracting landscape from banner uncropped.""" from app.library.Utils import get_channel_images @@ -1859,23 +1831,27 @@ class TestArgConverterAdvanced: def test_arg_converter_with_removed_options(self): """Test arg_converter with removed options tracking.""" - try: - removed = [] - result = arg_converter("--quiet --skip-download", level=True, removed_options=removed) + if importlib.util.find_spec("yt_dlp") is None: + with pytest.raises(ModuleNotFoundError): + arg_converter("--quiet --skip-download", level=True, removed_options=[]) + return - assert isinstance(result, dict) - except (ModuleNotFoundError, AttributeError, ImportError): - assert True + removed = [] + result = arg_converter("--quiet --skip-download", level=True, removed_options=removed) + + assert isinstance(result, dict) + assert removed def test_arg_converter_dumps_enabled(self): """Test arg_converter with dumps flag enabled.""" - try: - result = arg_converter("--format best", dumps=True) + if importlib.util.find_spec("yt_dlp") is None: + with pytest.raises(ModuleNotFoundError): + arg_converter("--format best", dumps=True) + return - # With dumps=True, result should still be a dict with JSON-serializable content - assert isinstance(result, (dict, list)) - except (ModuleNotFoundError, AttributeError, ImportError): - assert True + result = arg_converter("--format best", dumps=True) + + assert isinstance(result, (dict, list)) class TestCreateCookiesFile: @@ -1910,7 +1886,7 @@ class TestCreateCookiesFile: @patch("app.library.config.Config") @patch("app.library.Utils.load_cookies") - def test_create_cookies_file_without_path(self, mock_load_cookies, mock_config): + def test_create_cookies_file_auto_path(self, mock_load_cookies, mock_config): """Test creating a cookies file without a specific path (auto temp file).""" from app.library.Utils import create_cookies_file @@ -1940,7 +1916,7 @@ class TestCreateCookiesFile: create_cookies_file("invalid_data", file=cookie_path) @patch("app.library.Utils.load_cookies") - def test_create_cookies_file_creates_parent_directory(self, mock_load_cookies): + def test_create_cookies_parent_dir(self, mock_load_cookies): """Test that create_cookies_file creates parent directories as needed.""" from app.library.Utils import create_cookies_file @@ -1955,7 +1931,7 @@ class TestCreateCookiesFile: assert cookie_path.parent == Path(self.test_path / "a" / "b" / "c") @patch("app.library.Utils.load_cookies") - def test_create_cookies_file_with_special_characters(self, mock_load_cookies): + def test_create_cookies_special_chars(self, mock_load_cookies): """Test create_cookies_file with special characters in cookie data.""" from app.library.Utils import create_cookies_file @@ -2099,7 +2075,7 @@ class TestRenameFile: assert subtitle_file.exists() assert conflicting_sidecar.exists() - def test_rename_preserves_sidecar_extensions(self, tmp_path: Path): + def test_rename_sidecar_extensions(self, tmp_path: Path): """Test that rename preserves complex sidecar extensions.""" # Create test files with complex extensions test_file = tmp_path / "video.mp4" diff --git a/ui/tests/composables/useConditions.test.ts b/ui/tests/composables/useConditions.test.ts index 877cdc00..c0c31f9b 100644 --- a/ui/tests/composables/useConditions.test.ts +++ b/ui/tests/composables/useConditions.test.ts @@ -78,7 +78,7 @@ describe('useConditions', () => { }) describe('loadConditions', () => { - it('loads conditions with pagination successfully', async () => { + it('load_conditions', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -101,7 +101,7 @@ describe('useConditions', () => { requestSpy.mockRestore() }) - it('sorts conditions by priority then name', async () => { + it('sort_priority', async () => { const items = [ { ...mockCondition, id: 1, name: 'B', priority: 2 }, { ...mockCondition, id: 2, name: 'A', priority: 2 }, @@ -136,7 +136,7 @@ describe('useConditions', () => { }) describe('getCondition', () => { - it('fetches a single condition successfully', async () => { + it('get_condition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -157,7 +157,7 @@ describe('useConditions', () => { }) describe('createCondition', () => { - it('creates a condition successfully', async () => { + it('create_condition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -183,7 +183,7 @@ describe('useConditions', () => { }) describe('updateCondition', () => { - it('updates a condition successfully', async () => { + it('update_condition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -202,7 +202,7 @@ describe('useConditions', () => { requestSpy.mockRestore() }) - it('removes id field from condition before sending', async () => { + it('strip_update_id', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -216,13 +216,13 @@ describe('useConditions', () => { await conditions.updateCondition(1, mockCondition) const requestBody = JSON.parse((requestSpy.mock.calls[0][1] as any).body) - expect(requestBody.id).toBeUndefined() + expect('id' in requestBody).toBe(false) requestSpy.mockRestore() }) }) describe('patchCondition', () => { - it('patches a condition successfully', async () => { + it('patch_condition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -242,7 +242,7 @@ describe('useConditions', () => { }) describe('deleteCondition', () => { - it('deletes a condition successfully', async () => { + it('delete_condition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -265,7 +265,7 @@ describe('useConditions', () => { }) describe('testCondition', () => { - it('tests a condition successfully', async () => { + it('test_condition', async () => { const testResponse = { status: true, condition: 'duration > 60', @@ -292,7 +292,7 @@ describe('useConditions', () => { requestSpy.mockRestore() }) - it('handles test errors', async () => { + it('store_test_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -309,7 +309,7 @@ describe('useConditions', () => { }) expect(result).toBeNull() - expect(conditions.lastError.value).toBeTruthy() + expect(conditions.lastError.value).toBe('Invalid URL') requestSpy.mockRestore() }) }) diff --git a/ui/tests/composables/useConsoleSession.test.ts b/ui/tests/composables/useConsoleSession.test.ts index f670a8ac..f42e64f4 100644 --- a/ui/tests/composables/useConsoleSession.test.ts +++ b/ui/tests/composables/useConsoleSession.test.ts @@ -109,7 +109,7 @@ afterEach(() => { }) describe('useConsoleSession', () => { - it('starts a session, persists prompt transcript, and stores streamed output', async () => { + it('start_session', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -166,7 +166,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('restores a running session using both since and Last-Event-ID', async () => { + it('restore_with_cursor', async () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-restore', @@ -196,7 +196,6 @@ describe('useConsoleSession', () => { expect(fetchEventSourceMock).toHaveBeenCalledTimes(1) const streamCall = fetchEventSourceMock.mock.calls[0] - expect(streamCall).toBeDefined() if (!streamCall) { throw new Error('Expected fetchEventSource to be called once.') } @@ -209,7 +208,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('marks the session expired and stops retry setup on stream not found', async () => { + it('mark_expired_404', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -243,7 +242,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('restores a persisted session even after a local stream error state', async () => { + it('restore_after_stream_error', async () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-reconnect', @@ -277,7 +276,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('deduplicates replayed events when reconnect resumes from the last event id', async () => { + it('dedupe_replayed_events', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -333,7 +332,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('refreshes final metadata after close so interrupted sessions keep their backend status', async () => { + it('refresh_final_status', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -382,7 +381,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('requests cancellation for the active session without dropping local state early', async () => { + it('cancel_without_reset', async () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-stop', @@ -413,12 +412,12 @@ describe('useConsoleSession', () => { const deleteCall = requestSpy.mock.calls.find( (call) => call[0] === '/api/system/terminal/sess-stop' && call[1]?.method === 'DELETE', ) - expect(deleteCall).toBeDefined() + expect(deleteCall).toEqual(['/api/system/terminal/sess-stop', { method: 'DELETE' }]) requestSpy.mockRestore() }) - it('disconnects the local stream without issuing a cancel request', async () => { + it('disconnect_local', async () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-detach', @@ -449,7 +448,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('refreshes metadata when cancel targets a missing session', async () => { + it('refresh_missing_cancel', async () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-missing', @@ -478,7 +477,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('clears visible transcript without dropping the active session cursor', () => { + it('clear_transcript', () => { const session = useConsoleSession() session.state.value = { sessionId: 'sess-active', @@ -498,7 +497,7 @@ describe('useConsoleSession', () => { expect(session.state.value.lastEventId).toBe('15') }) - it('fetches recent sessions and filters locally hidden entries', async () => { + it('filter_hidden_recents', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -557,7 +556,7 @@ describe('useConsoleSession', () => { requestSpy.mockRestore() }) - it('replays a recent session by reconnecting to its stream', async () => { + it('replay_recent_session', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ diff --git a/ui/tests/composables/useDirtyCloseGuard.test.ts b/ui/tests/composables/useDirtyCloseGuard.test.ts index 67df57ed..25d91e04 100644 --- a/ui/tests/composables/useDirtyCloseGuard.test.ts +++ b/ui/tests/composables/useDirtyCloseGuard.test.ts @@ -55,7 +55,7 @@ afterEach(() => { }); describe('useDirtyCloseGuard', () => { - it('confirms discard on route leave when the editor is open and dirty', async () => { + it('confirm_route_leave', async () => { const open = ref(true); const dirty = ref(true); const onDiscard = mock(() => {}); @@ -75,7 +75,7 @@ describe('useDirtyCloseGuard', () => { expect(open.value).toBe(false); }); - it('blocks route updates when the user keeps editing', async () => { + it('block_route_update', async () => { const open = ref(true); const dirty = ref(true); @@ -94,7 +94,7 @@ describe('useDirtyCloseGuard', () => { expect(open.value).toBe(true); }); - it('only prevents browser unload while the editor is open and dirty, then removes the listener on unmount', () => { + it('guard_beforeunload', () => { const open = ref(true); const dirty = ref(true); @@ -130,7 +130,7 @@ describe('useDirtyCloseGuard', () => { expect(afterUnmountEvent.defaultPrevented).toBe(false); }); - it('dedupes concurrent close requests into one discard dialog', async () => { + it('dedupe_close_prompt', async () => { const open = ref(true); const dirty = ref(true); const onDiscard = mock(() => {}); @@ -162,7 +162,7 @@ describe('useDirtyCloseGuard', () => { expect(open.value).toBe(false); }); - it('does not guard route changes when the editor is clean', async () => { + it('skip_clean_editor', async () => { const open = ref(true); const dirty = ref(false); diff --git a/ui/tests/composables/usePlayerShortcuts.test.ts b/ui/tests/composables/usePlayerShortcuts.test.ts index fc557fcb..7c90afb6 100644 --- a/ui/tests/composables/usePlayerShortcuts.test.ts +++ b/ui/tests/composables/usePlayerShortcuts.test.ts @@ -10,7 +10,7 @@ describe('usePlayerShortcuts', () => { document.body.innerHTML = ''; }); - it('toggles native text tracks and subtitle state on c', async () => { + it('toggle_subs_c', async () => { const { usePlayerShortcuts } = await import('~/composables/usePlayerShortcuts'); const addEventListenerSpy = spyOn(document, 'addEventListener'); @@ -63,7 +63,7 @@ describe('usePlayerShortcuts', () => { addEventListenerSpy.mockRestore(); }); - it('closes help before closing the player on escape', async () => { + it('close_help_first', async () => { const { usePlayerShortcuts } = await import('~/composables/usePlayerShortcuts'); const media = { diff --git a/ui/tests/composables/usePlayerSubtitles.test.ts b/ui/tests/composables/usePlayerSubtitles.test.ts index 0b22d6bb..c27cebc8 100644 --- a/ui/tests/composables/usePlayerSubtitles.test.ts +++ b/ui/tests/composables/usePlayerSubtitles.test.ts @@ -85,7 +85,7 @@ describe('usePlayerSubtitles', () => { delete (globalThis as { fetch?: typeof fetch }).fetch; }); - it('loads subtitle manifest and exposes the preferred native track', async () => { + it('load_native_track', async () => { fetchMock.mockResolvedValueOnce( createMockResponse({ ok: true, @@ -140,7 +140,7 @@ describe('usePlayerSubtitles', () => { expect(usesAssSubtitleTrack.value).toBe(false); }); - it('uses the provided full manifest path including folders', async () => { + it('keep_manifest_path', async () => { fetchMock.mockResolvedValueOnce( createMockResponse({ ok: true, @@ -170,7 +170,7 @@ describe('usePlayerSubtitles', () => { ); }); - it('creates and destroys an ASS renderer for ASS subtitles when playback becomes active', async () => { + it('mount_ass_renderer', async () => { fetchMock.mockResolvedValueOnce( createMockResponse({ ok: true, @@ -238,7 +238,7 @@ describe('usePlayerSubtitles', () => { expect(assDestroyMock.mock.calls.length).toBeGreaterThanOrEqual(1); }); - it('recreates an ASS renderer when the layout version changes without refetching subtitles', async () => { + it('remount_on_layout_change', async () => { fetchMock.mockResolvedValueOnce( createMockResponse({ ok: true, diff --git a/ui/tests/composables/usePresetOptions.test.ts b/ui/tests/composables/usePresetOptions.test.ts index 5c26598c..37eef50b 100644 --- a/ui/tests/composables/usePresetOptions.test.ts +++ b/ui/tests/composables/usePresetOptions.test.ts @@ -40,7 +40,7 @@ const setConfigStore = (presets: Preset[]) => { } describe('usePresetOptions', () => { - it('groups custom presets before default presets by default', () => { + it('group_custom_first', () => { setConfigStore([ buildPreset('default_video', true), buildPreset('custom_audio', false), @@ -56,7 +56,7 @@ describe('usePresetOptions', () => { ]) }) - it('supports default-first grouping when requested', () => { + it('group_default_first', () => { setConfigStore([ buildPreset('default_video', true), buildPreset('custom_audio', false), diff --git a/ui/tests/composables/usePresets.test.ts b/ui/tests/composables/usePresets.test.ts index e6d1e95c..6294b49e 100644 --- a/ui/tests/composables/usePresets.test.ts +++ b/ui/tests/composables/usePresets.test.ts @@ -81,7 +81,7 @@ describe('usePresets', () => { }) describe('loadPresets', () => { - it('loads presets with pagination successfully', async () => { + it('load_presets', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -104,7 +104,7 @@ describe('usePresets', () => { requestSpy.mockRestore() }) - it('requests custom presets without defaults when asked', async () => { + it('exclude_defaults', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -124,7 +124,7 @@ describe('usePresets', () => { requestSpy.mockRestore() }) - it('sorts presets by priority then name', async () => { + it('sort_priority', async () => { const items = [ { ...mockPreset, id: 1, name: 'B', priority: 2 }, { ...mockPreset, id: 2, name: 'A', priority: 2 }, @@ -159,7 +159,7 @@ describe('usePresets', () => { }) describe('getPreset', () => { - it('fetches a single preset successfully', async () => { + it('get_preset', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -180,7 +180,7 @@ describe('usePresets', () => { }) describe('createPreset', () => { - it('creates a preset successfully', async () => { + it('create_preset', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -209,7 +209,7 @@ describe('usePresets', () => { }) describe('updatePreset', () => { - it('updates a preset successfully', async () => { + it('update_preset', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -226,7 +226,7 @@ describe('usePresets', () => { requestSpy.mockRestore() }) - it('strips id from update payload', async () => { + it('strip_update_id', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -240,14 +240,14 @@ describe('usePresets', () => { await presets.updatePreset(1, { ...mockPreset, default: true }) const requestBody = JSON.parse((requestSpy.mock.calls[0]![1] as any).body) - expect(requestBody.id).toBeUndefined() + expect('id' in requestBody).toBe(false) expect(requestBody.default).toBe(false) requestSpy.mockRestore() }) }) describe('patchPreset', () => { - it('patches a preset successfully', async () => { + it('patch_preset', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -264,7 +264,7 @@ describe('usePresets', () => { requestSpy.mockRestore() }) - it('strips id and default from patch payload', async () => { + it('sanitize_patch', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -278,14 +278,14 @@ describe('usePresets', () => { await presets.patchPreset(1, { id: 10, default: true }) const requestBody = JSON.parse((requestSpy.mock.calls[0]![1] as any).body) - expect(requestBody.id).toBeUndefined() + expect('id' in requestBody).toBe(false) expect(requestBody.default).toBe(false) requestSpy.mockRestore() }) }) describe('deletePreset', () => { - it('deletes a preset successfully', async () => { + it('delete_preset', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ diff --git a/ui/tests/composables/useTaskDefinitions.test.ts b/ui/tests/composables/useTaskDefinitions.test.ts index 2b69994e..abf438eb 100644 --- a/ui/tests/composables/useTaskDefinitions.test.ts +++ b/ui/tests/composables/useTaskDefinitions.test.ts @@ -72,7 +72,7 @@ describe('useTaskDefinitions', () => { errorMock.mockClear() }) - it('sorts definitions by priority then name', async () => { + it('sort_priority', async () => { const items = [ { id: 1, name: 'B', priority: 2, updated_at: 1 }, { id: 2, name: 'A', priority: 2, updated_at: 2 }, @@ -90,7 +90,7 @@ describe('useTaskDefinitions', () => { requestSpy.mockRestore() }) - it('handles empty payload', async () => { + it('handle_empty_payload', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce(createMockResponse({ ok: true, @@ -104,7 +104,7 @@ describe('useTaskDefinitions', () => { requestSpy.mockRestore() }) - it('throws loadDefinitions error when throwInstead is true', async () => { + it('throw_load_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce(createMockResponse({ ok: false, @@ -117,7 +117,7 @@ describe('useTaskDefinitions', () => { requestSpy.mockRestore() }) - it('returns null on getDefinition error', async () => { + it('store_get_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce(createMockResponse({ ok: false, @@ -132,7 +132,7 @@ describe('useTaskDefinitions', () => { requestSpy.mockRestore() }) - it('creates definition successfully', async () => { + it('create_definition', async () => { const payload: TaskDefinitionDetailed = { id: 2, name: 'New', @@ -152,7 +152,7 @@ describe('useTaskDefinitions', () => { requestSpy.mockRestore() }) - it('removes definition on deleteDefinition', async () => { + it('remove_deleted_definition', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce(createMockResponse({ ok: true, diff --git a/ui/tests/composables/useTasks.test.ts b/ui/tests/composables/useTasks.test.ts index 4dbd800b..8940cbb4 100644 --- a/ui/tests/composables/useTasks.test.ts +++ b/ui/tests/composables/useTasks.test.ts @@ -85,7 +85,7 @@ describe('useTasks', () => { }) describe('loadTasks', () => { - it('loads tasks with pagination successfully', async () => { + it('load_tasks', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -108,7 +108,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('sorts tasks by name A-Z', async () => { + it('sort_name', async () => { const items = [ { ...mockTask, id: 1, name: 'Zebra' }, { ...mockTask, id: 2, name: 'Alpha' }, @@ -137,7 +137,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles empty tasks list', async () => { + it('handle_empty_list', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -158,7 +158,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles errors during load', async () => { + it('store_load_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -171,11 +171,11 @@ describe('useTasks', () => { const tasks = useTasks() await tasks.loadTasks() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Server error') requestSpy.mockRestore() }) - it('respects page and per_page parameters', async () => { + it('pass_pagination', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -197,7 +197,7 @@ describe('useTasks', () => { }) describe('getTask', () => { - it('fetches a single task successfully', async () => { + it('get_task', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -215,7 +215,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles 404 not found', async () => { + it('store_404_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -229,13 +229,13 @@ describe('useTasks', () => { const result = await tasks.getTask(999) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Task not found') requestSpy.mockRestore() }) }) describe('createTask', () => { - it('creates a task successfully', async () => { + it('create_task', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -261,7 +261,7 @@ describe('useTasks', () => { }) describe('updateTask', () => { - it('updates a task successfully', async () => { + it('update_task', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -280,7 +280,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('removes id field from task before sending', async () => { + it('strip_update_id', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -301,7 +301,7 @@ describe('useTasks', () => { }) describe('patchTask', () => { - it('patches a task successfully', async () => { + it('patch_task', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -321,7 +321,7 @@ describe('useTasks', () => { }) describe('deleteTask', () => { - it('deletes a task successfully', async () => { + it('delete_task', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -344,7 +344,7 @@ describe('useTasks', () => { }) describe('inspectTaskHandler', () => { - it('inspects task handler successfully', async () => { + it('inspect_handler', async () => { const inspectResponse: TaskInspectResponse = { matched: true, handler: 'YoutubeHandler', @@ -383,7 +383,7 @@ describe('useTasks', () => { fetchSpy.mockRestore() }) - it('handles handler not supported', async () => { + it('handle_unsupported_handler', async () => { const inspectResponse: TaskInspectResponse = { matched: false, handler: null, @@ -411,7 +411,7 @@ describe('useTasks', () => { fetchSpy.mockRestore() }) - it('handles inspect errors', async () => { + it('store_inspect_error', async () => { const fetchSpy = spyOn(globalThis, 'fetch') fetchSpy.mockRejectedValueOnce(new Error('Network error')) @@ -421,13 +421,13 @@ describe('useTasks', () => { }) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Network error') fetchSpy.mockRestore() }) }) describe('markTaskItems', () => { - it('marks all items as downloaded successfully', async () => { + it('mark_items', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -445,7 +445,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles mark errors', async () => { + it('store_mark_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -459,13 +459,13 @@ describe('useTasks', () => { const result = await tasks.markTaskItems(999) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Task not found') requestSpy.mockRestore() }) }) describe('unmarkTaskItems', () => { - it('unmarks items successfully', async () => { + it('unmark_items', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -483,7 +483,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles unmark errors', async () => { + it('store_unmark_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -497,13 +497,13 @@ describe('useTasks', () => { const result = await tasks.unmarkTaskItems(999) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Task not found') requestSpy.mockRestore() }) }) describe('generateTaskMetadata', () => { - it('generates metadata successfully', async () => { + it('generate_metadata', async () => { const metadataResponse: TaskMetadataResponse = { status: 'success', generated: ['tvshow.nfo', 'info.json', 'poster.jpg'], @@ -527,7 +527,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('handles metadata generation errors', async () => { + it('store_metadata_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -541,13 +541,13 @@ describe('useTasks', () => { const result = await tasks.generateTaskMetadata(1) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Failed to generate metadata') requestSpy.mockRestore() }) }) describe('error handling', () => { - it('throws when throwInstead is true', async () => { + it('throw_on_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -564,7 +564,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('clears error on clearError call', async () => { + it('clear_error', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -579,7 +579,7 @@ describe('useTasks', () => { await tasks.loadTasks() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('Server error') tasks.clearError() @@ -587,7 +587,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('parses API validation errors correctly', async () => { + it('parse_validation_errors', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -611,7 +611,7 @@ describe('useTasks', () => { const result = await tasks.createTask(newTask) expect(result).toBeNull() - expect(tasks.lastError.value).toBeTruthy() + expect(tasks.lastError.value).toBe('name: Field required, url: Invalid URL format') expect(tasks.lastError.value).toContain('name: Field required') expect(tasks.lastError.value).toContain('url: Invalid URL format') requestSpy.mockRestore() @@ -619,7 +619,7 @@ describe('useTasks', () => { }) describe('addInProgress state', () => { - it('sets addInProgress during create operation', async () => { + it('set_add_in_progress', async () => { let inProgressDuringCall = false const requestSpy = spyOn(utils, 'request') @@ -644,7 +644,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('resets addInProgress on error', async () => { + it('reset_add_in_progress', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -666,7 +666,7 @@ describe('useTasks', () => { }) describe('isLoading state', () => { - it('sets isLoading during loadTasks operation', async () => { + it('set_loading', async () => { let loadingDuringCall = false const requestSpy = spyOn(utils, 'request') @@ -693,7 +693,7 @@ describe('useTasks', () => { }) describe('task list updates', () => { - it('updates existing task in list', async () => { + it('update_list_item', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ @@ -727,7 +727,7 @@ describe('useTasks', () => { requestSpy.mockRestore() }) - it('removes deleted task from list', async () => { + it('remove_list_item', async () => { const requestSpy = spyOn(utils, 'request') requestSpy.mockResolvedValueOnce( createMockResponse({ diff --git a/ui/tests/plugins/modal-opacity.client.test.ts b/ui/tests/plugins/modal-opacity.client.test.ts index dc46b2b4..e1f0a986 100644 --- a/ui/tests/plugins/modal-opacity.client.test.ts +++ b/ui/tests/plugins/modal-opacity.client.test.ts @@ -63,7 +63,7 @@ afterEach(async () => { }); describe('modal opacity plugin', () => { - it('ignores a settings-only overlay', async () => { + it('ignore_settings_overlay', async () => { startPlugin(); const settingsPanel = document.createElement('div'); @@ -77,7 +77,7 @@ describe('modal opacity plugin', () => { expect(syncOpacityMock).not.toHaveBeenCalled(); }); - it('restores opacity when a normal overlay closes back to settings-only', async () => { + it('restore_after_close', async () => { startPlugin(); const settingsPanel = document.createElement('div'); @@ -99,7 +99,7 @@ describe('modal opacity plugin', () => { expect(enableOpacityMock).toHaveBeenCalledTimes(1); }); - it('resyncs opacity when overlays change while already locked', async () => { + it('resync_while_locked', async () => { startPlugin(); document.body.append(createOverlay()); @@ -112,7 +112,7 @@ describe('modal opacity plugin', () => { expect(syncOpacityMock).toHaveBeenCalledTimes(1); }); - it('does not unlock opacity on beforeunload when reload is canceled', async () => { + it('keep_lock_beforeunload', async () => { startPlugin(); document.body.append(createOverlay()); diff --git a/ui/tests/utils/index.test.ts b/ui/tests/utils/index.test.ts index 8dd9b469..1b86183a 100644 --- a/ui/tests/utils/index.test.ts +++ b/ui/tests/utils/index.test.ts @@ -132,81 +132,81 @@ afterEach(() => { }); describe('object access helpers', () => { - it('ag returns nested value or default value', () => { + it('ag_nested_value', () => { const payload = { a: { b: { c: 42 } } }; expect(utils.ag(payload, 'a.b.c')).toBe(42); expect(utils.ag(payload, 'a.b.x', 'fallback')).toBe('fallback'); expect(utils.ag(payload, 'missing', () => 'fn-default')).toBe('fn-default'); }); - it('ag_set sets nested path creating objects as needed', () => { + it('ag_set_path', () => { const payload: Record = {}; utils.ag_set(payload, 'a.b.c', 99); expect(payload).toEqual({ a: { b: { c: 99 } } }); }); - it('cleanObject removes requested keys', () => { + it('clean_object_keys', () => { const source = { id: 1, keep: true, drop: false }; expect(utils.cleanObject(source, ['drop'])).toEqual({ id: 1, keep: true }); expect(utils.cleanObject(source, [])).toEqual(source); }); - it('stripPath removes base prefix and leading slashes', () => { + it('strip_path_base', () => { expect(utils.stripPath('/data/downloads', '/data/downloads/video.mp4')).toBe('video.mp4'); expect(utils.stripPath('', '/var/files/test.txt')).toBe('/var/files/test.txt'); }); }); describe('string manipulation helpers', () => { - it('r replaces tokens with context values', () => { + it('replace_tokens', () => { const result = utils.r('Hello {user.name}!', { user: { name: 'YTPTube' } }); expect(result).toBe('Hello YTPTube!'); }); - it('iTrim trims delimiters at requested positions', () => { + it('itrim_edges', () => { expect(utils.iTrim('--value--', '-', 'both')).toBe('value'); expect(utils.iTrim('::value', ':', 'start')).toBe('value'); expect(utils.iTrim('value::', ':', 'end')).toBe('value'); }); - it('iTrim handles forward slash delimiter', () => { + it('itrim_slash', () => { expect(utils.iTrim('//value//', '/', 'both')).toBe('value'); expect(utils.iTrim('/value', '/', 'start')).toBe('value'); expect(utils.iTrim('value/', '/', 'end')).toBe('value'); expect(utils.iTrim('///multiple///', '/', 'both')).toBe('multiple'); }); - it('iTrim handles backslash delimiter', () => { + it('itrim_backslash', () => { expect(utils.iTrim('\\\\value\\\\', '\\', 'both')).toBe('value'); expect(utils.iTrim('\\value', '\\', 'start')).toBe('value'); expect(utils.iTrim('value\\', '\\', 'end')).toBe('value'); }); - it('iTrim handles hyphen delimiter', () => { + it('itrim_hyphen', () => { expect(utils.iTrim('--value--', '-', 'both')).toBe('value'); expect(utils.iTrim('-value', '-', 'start')).toBe('value'); expect(utils.iTrim('value-', '-', 'end')).toBe('value'); expect(utils.iTrim('---multiple---', '-', 'both')).toBe('multiple'); }); - it('iTrim handles caret delimiter', () => { + it('itrim_caret', () => { expect(utils.iTrim('^^value^^', '^', 'both')).toBe('value'); expect(utils.iTrim('^value', '^', 'start')).toBe('value'); expect(utils.iTrim('value^', '^', 'end')).toBe('value'); }); - it('iTrim handles bracket delimiters', () => { + it('itrim_brackets', () => { expect(utils.iTrim('[[value]]', '[', 'both')).toBe('value]]'); expect(utils.iTrim(']]value[[', ']', 'both')).toBe('value[['); }); - it('iTrim handles dot delimiter', () => { + it('itrim_dot', () => { expect(utils.iTrim('..value..', '.', 'both')).toBe('value'); expect(utils.iTrim('.value', '.', 'start')).toBe('value'); expect(utils.iTrim('value.', '.', 'end')).toBe('value'); }); - it('iTrim handles special regex characters', () => { + it('itrim_regex_chars', () => { expect(utils.iTrim('**value**', '*', 'both')).toBe('value'); expect(utils.iTrim('++value++', '+', 'both')).toBe('value'); expect(utils.iTrim('??value??', '?', 'both')).toBe('value'); @@ -215,96 +215,96 @@ describe('string manipulation helpers', () => { expect(utils.iTrim('((value))', ')', 'both')).toBe('((value'); }); - it('iTrim handles empty string', () => { + it('itrim_empty', () => { expect(utils.iTrim('', '/', 'both')).toBe(''); }); - it('iTrim throws error when delimiter is empty', () => { + it('itrim_empty_delimiter', () => { expect(() => utils.iTrim('value', '', 'both')).toThrow('Delimiter is required'); }); - it('iTrim preserves middle occurrences', () => { + it('itrim_preserve_middle', () => { expect(utils.iTrim('/path/to/file/', '/', 'both')).toBe('path/to/file'); expect(utils.iTrim('//path//to//file//', '/', 'both')).toBe('path//to//file'); }); - it('encodePath safely encodes components', () => { + it('encode_path', () => { expect(utils.encodePath('folder#1/video name.mp4')).toBe('folder%231/video%20name.mp4'); }); - it('encodePath handles % character correctly', () => { + it('encode_percent', () => { expect(utils.encodePath('How to enjoy Shin Ramyun 100%.opus')).toBe( 'How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus', ); }); - it('encodePath handles multiple special characters', () => { + it('encode_specials', () => { expect(utils.encodePath('100% complete [HD] #1.mp4')).toBe( '100%25%20complete%20%5BHD%5D%20%231.mp4', ); }); - it('encodePath handles paths with % character', () => { + it('keep_segments', () => { expect(utils.encodePath('folder/How to enjoy Shin Ramyun 100%.opus')).toBe( 'folder/How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus', ); }); - it('encodePath handles already encoded strings', () => { + it('preserve_encoded', () => { expect(utils.encodePath('How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus')).toBe( 'How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus', ); }); - it('encodePath handles mixed encoded and unencoded', () => { + it('mix_encoded_input', () => { expect(utils.encodePath('folder/file%20name 100%.mp4')).toBe('folder/file%20name%20100%25.mp4'); }); - it('encodePath handles special characters &, =, ?', () => { + it('encode_query_chars', () => { expect(utils.encodePath('query?param=value&key=100%.mp4')).toBe( 'query%3Fparam%3Dvalue%26key%3D100%25.mp4', ); }); - it('encodePath handles empty string', () => { + it('encode_empty', () => { expect(utils.encodePath('')).toBe(''); }); - it('encodePath handles simple filename', () => { + it('encode_simple_filename', () => { expect(utils.encodePath('video.mp4')).toBe('video.mp4'); }); - it('encodePath handles unicode characters', () => { + it('encode_unicode', () => { expect(utils.encodePath('视频文件.mp4')).toBe('%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6.mp4'); }); - it('encodePath handles parentheses', () => { + it('encode_parentheses', () => { expect(utils.encodePath('video (1080p).mp4')).toBe('video%20(1080p).mp4'); }); - it('removeANSIColors strips escape codes', () => { + it('strip_ansi', () => { const sample = '\u001b[31mError\u001b[0m'; expect(utils.removeANSIColors(sample)).toBe('Error'); }); - it('basename returns final segment optionally trimming extension', () => { + it('basename_trim_ext', () => { expect(utils.basename('/downloads/video.mp4')).toBe('video.mp4'); expect(utils.basename('/downloads/video.mp4', '.mp4')).toBe('video'); expect(utils.basename('', '.mp4')).toBe(''); }); - it('dirname returns parent directory', () => { + it('dirname_parent', () => { expect(utils.dirname('/downloads/video.mp4')).toBe('/downloads'); expect(utils.dirname('video.mp4')).toBe('.'); expect(utils.dirname('/file')).toBe('/'); }); - it('formatBytes returns human readable strings', () => { + it('format_bytes', () => { expect(utils.formatBytes(0)).toBe('0 Bytes'); expect(utils.formatBytes(1024)).toBe('1 KiB'); }); - it('formatTime renders hh:mm:ss or mm:ss', () => { + it('format_time', () => { expect(utils.formatTime(59)).toBe('59'); expect(utils.formatTime(90)).toBe('01:30'); expect(utils.formatTime(3661)).toBe('01:01:01'); @@ -312,47 +312,47 @@ describe('string manipulation helpers', () => { }); describe('data conversion helpers', () => { - it('has_data detects arrays, objects, and json strings', () => { + it('has_data', () => { expect(utils.has_data({ key: 'value' })).toBe(true); expect(utils.has_data('""')).toBe(false); expect(utils.has_data('[1,2]')).toBe(true); expect(utils.has_data('')).toBe(false); }); - it('encode and decode provide reversible transformation', () => { + it('encode_decode_roundtrip', () => { const payload = { name: 'YTPTube', count: 2 }; const encoded = utils.encode(payload); expect(typeof encoded).toBe('string'); expect(utils.decode(encoded)).toEqual(payload); }); - it('getQueryParams parses query strings', () => { + it('parse_query_params', () => { expect(utils.getQueryParams('?a=1&b=two')).toEqual({ a: '1', b: 'two' }); }); - it('uri prefixes runtime base path', () => { + it('prefix_runtime_base', () => { runtimeConfig.app.baseURL = '/base-path'; expect(utils.uri('/api/test')).toBe('/base-path/api/test'); runtimeConfig.app.baseURL = '/'; expect(utils.uri('/api/test')).toBe('/api/test'); }); - it('makeDownload builds expected url with folder and filename', () => { + it('build_file_url', () => { runtimeConfig.app.baseURL = '/base-path'; const url = utils.makeDownload({}, { folder: 'music', filename: 'song.mp3' }); expect(url).toBe('/base-path/api/download/music/song.mp3'); }); - it('makeDownload handles m3u8 base path', () => { + it('build_m3u8_url', () => { const url = utils.makeDownload({}, { filename: 'playlist' }, 'm3u8'); expect(url).toBe('/base-path/api/player/m3u8/video/playlist.m3u8'); }); - it('isDownloadSkipped detects finished skipped-download items', () => { + it('detect_download_skipped', () => { expect(utils.isDownloadSkipped({ status: 'finished', download_skipped: true } as any)).toBe(true); }); - it('isDownloadSkipped ignores unfinished or unflagged items', () => { + it('ignore_non_skipped', () => { expect(utils.isDownloadSkipped({ status: 'finished', download_skipped: false } as any)).toBe(false); expect(utils.isDownloadSkipped({ status: 'downloading', download_skipped: true } as any)).toBe(false); expect(utils.isDownloadSkipped(undefined as any)).toBe(false); @@ -360,7 +360,7 @@ describe('data conversion helpers', () => { }); describe('dom and browser helpers', () => { - it('copyText uses clipboard API and notifies success', async () => { + it('write_clipboard', async () => { utils.copyText('sample'); await Promise.resolve(); @@ -372,13 +372,13 @@ describe('dom and browser helpers', () => { expect(notificationMock.success).toHaveBeenCalledWith('Text copied to clipboard.'); }); - it('disableOpacity toggles body opacity when enabled', () => { + it('lock_opacity', () => { const result = utils.disableOpacity(); expect(result).toBe(true); expect(document.body.style.opacity).toBe('1'); }); - it('disableOpacity returns false when background disabled', () => { + it('skip_disabled_bg', () => { document.body.removeAttribute('style'); storageMap.clear(); const originalOpacity = document.body.style.opacity; @@ -398,7 +398,7 @@ describe('dom and browser helpers', () => { expect(document.body.style.opacity || '').toBe(originalOpacity || ''); }); - it('enableOpacity applies stored opacity value', () => { + it('restore_opacity', () => { utils.disableOpacity(); useStorageFn.mockImplementation((key: string, defaultValue: any) => { if (!storageMap.has(key)) { @@ -413,7 +413,7 @@ describe('dom and browser helpers', () => { expect(document.body.style.opacity).toBe('0.75'); }); - it('syncOpacity keeps full opacity while a lock is active', () => { + it('keep_opacity_lock', () => { utils.disableOpacity(); storageMap.set('random_bg_opacity', { value: 0.35 }); @@ -423,7 +423,7 @@ describe('dom and browser helpers', () => { expect(document.body.style.opacity).toBe('1'); }); - it('syncOpacity clears body opacity when background is disabled', () => { + it('clear_disabled_bg', () => { document.body.style.opacity = '0.8'; storageMap.set('random_bg', { value: false }); @@ -435,7 +435,7 @@ describe('dom and browser helpers', () => { }); describe('network and id helpers', () => { - it('request prefixes relative urls and sets defaults', async () => { + it('prefix_base_url', async () => { const responseMock = { status: 200 } as Response; fetchMock.mockResolvedValue(responseMock); @@ -452,7 +452,7 @@ describe('network and id helpers', () => { expect((options as Record).withCredentials).toBe(true); }); - it('convertCliOptions posts payload and returns parsed json', async () => { + it('post_cli_args', async () => { const jsonMock = mock().mockResolvedValue({ success: true }); const responseMock = { status: 200, json: jsonMock }; fetchMock.mockResolvedValue(responseMock); @@ -468,7 +468,7 @@ describe('network and id helpers', () => { expect(result).toEqual({ success: true }); }); - it('convertCliOptions throws on non-200 response', async () => { + it('throw_cli_error', async () => { const jsonMock = mock().mockResolvedValue({ error: 'fail' }); const responseMock = { status: 400, json: jsonMock }; fetchMock.mockResolvedValue(responseMock); @@ -479,13 +479,13 @@ describe('network and id helpers', () => { }); describe('async helpers', () => { - it('awaiter resolves when test becomes truthy', async () => { + it('resolve_truthy', async () => { const values = [false, false, 'done']; const result = await utils.awaiter(() => values.shift(), 500, 0.01); expect(result).toBe('done'); }); - it('awaiter returns false when timeout reached', async () => { + it('timeout', async () => { const result = await utils.awaiter(() => false, 50, 0.01); expect(result).toBe(false); }); diff --git a/ui/tests/utils/sidebarSwipe.test.ts b/ui/tests/utils/sidebarSwipe.test.ts index 8e1c299f..6669af9e 100644 --- a/ui/tests/utils/sidebarSwipe.test.ts +++ b/ui/tests/utils/sidebarSwipe.test.ts @@ -9,7 +9,7 @@ import { } from '~/utils/sidebarSwipe'; describe('sidebarSwipe', () => { - it('detects apple mobile webkit navigators', () => { + it('detect_apple_touch', () => { expect( isAppleMobileTouchNavigator({ userAgent: @@ -38,7 +38,7 @@ describe('sidebarSwipe', () => { ).toBe(false); }); - it('reserves the ios navigation edge and starts the sidebar swipe just inside it', () => { + it('reserve_ios_edge', () => { const nav = { userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 Version/18.4 Mobile/15E148 Safari/604.1', @@ -54,7 +54,7 @@ describe('sidebarSwipe', () => { ); }); - it('keeps the original left-edge open band on non-apple mobile browsers', () => { + it('keep_default_edge', () => { const nav = { userAgent: 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/147.0.0.0 Mobile Safari/537.36', @@ -67,7 +67,7 @@ describe('sidebarSwipe', () => { expect(canStartSidebarOpenSwipe(MOBILE_SIDEBAR_EDGE_WIDTH + 1, nav)).toBe(false); }); - it('returns close while the sidebar is already open', () => { + it('return_close', () => { expect(getSidebarSwipeMode(true, 4)).toBe('close'); }); }); diff --git a/ui/tests/utils/ytdlp.test.ts b/ui/tests/utils/ytdlp.test.ts index 33197ecb..e291a528 100644 --- a/ui/tests/utils/ytdlp.test.ts +++ b/ui/tests/utils/ytdlp.test.ts @@ -6,7 +6,7 @@ function normalize(filters: string[]): Set { } describe("MatchFilterParser", () => { - it("handles simple AND", () => { + it("handle_simple_and", () => { const parser = new MatchFilterParser("filesize>1000000 & duration<600"); expect(parser.evaluate({ filesize: 2_000_000, duration: 200 })).toBe(true); @@ -14,7 +14,7 @@ describe("MatchFilterParser", () => { expect(parser.evaluate({ filesize: 2_000_000, duration: 800 })).toBe(false); }); - it("handles OR", () => { + it("handle_or", () => { const parser = new MatchFilterParser("uploader='BBC' || uploader='NHK'"); expect(parser.evaluate({ uploader: "BBC" })).toBe(true); @@ -22,7 +22,7 @@ describe("MatchFilterParser", () => { expect(parser.evaluate({ uploader: "CNN" })).toBe(false); }); - it("handles grouping", () => { + it("handle_grouping", () => { const parser = new MatchFilterParser("(filesize>1000000 & duration<600) || uploader='BBC'"); expect(parser.evaluate({ filesize: 2_000_000, duration: 200 })).toBe(true); @@ -30,14 +30,14 @@ describe("MatchFilterParser", () => { expect(parser.evaluate({ filesize: 500_000, duration: 200, uploader: "CNN" })).toBe(false); }); - it("handles unary presence", () => { + it("handle_unary_presence", () => { expect(new MatchFilterParser("duration").evaluate({ duration: 100 })).toBe(true); expect(new MatchFilterParser("duration").evaluate({})).toBe(false); expect(new MatchFilterParser("!duration").evaluate({})).toBe(true); expect(new MatchFilterParser("!duration").evaluate({ duration: 100 })).toBe(false); }); - it("parses duration with numeric values", () => { + it("parse_duration", () => { expect(new MatchFilterParser("duration<120").evaluate({ duration: 30 })).toBe(true); expect(new MatchFilterParser("duration<120").evaluate({ duration: 200 })).toBe(false); @@ -48,7 +48,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser("duration<3600").evaluate({ duration: 3700 })).toBe(false); }); - it("parses filesize with numeric values", () => { + it("parse_filesize", () => { expect(new MatchFilterParser("filesize>1000000").evaluate({ filesize: 2_000_000 })).toBe(true); expect(new MatchFilterParser("filesize>1000000").evaluate({ filesize: 500_000 })).toBe(false); @@ -56,7 +56,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser("filesize>=1073741824").evaluate({ filesize: 1000000 })).toBe(false); }); - it("handles string operators", () => { + it("handle_string_operators", () => { const d = { uploader: "BBC News Channel" }; expect(new MatchFilterParser("uploader*='News'").evaluate(d)).toBe(true); @@ -70,7 +70,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser("uploader~='News\\s+Network'").evaluate(d)).toBe(false); }); - it("handles spaces around operators", () => { + it("handle_operator_spaces", () => { const d = { "channel_id": "UC-7oMv6E4Uz2tF51w5Sj49w", "uploader": "BBC" @@ -85,7 +85,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser("channel_id = 'UCfmrcEdes7yDtEISGPM1T-A' & availability = subscriber_only").evaluate(d)).toBe(false); }); - it("reproduces original bug report", () => { + it("reproduce_bug_report", () => { const testData = { "age_limit": 0, "comment_count": 6, @@ -102,7 +102,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser("channel_id = 'UC-7oMv6E4Uz2tF51w5Sj49w'").evaluate(testData)).toBe(true); }); - it("tests OR operator precedence", () => { + it("or_precedence", () => { const testData = { "age_limit": 0, "fps": 120, @@ -134,7 +134,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser(expr2).evaluate(testDataEdge)).toBe(true); }); - it("tests complex OR precedence scenarios", () => { + it("handle_chained_or", () => { const testDataOrOnly = { "age_limit": 1, "fps": 60, @@ -163,7 +163,7 @@ describe("MatchFilterParser", () => { expect(new MatchFilterParser(exprChain).evaluate(testDataOnlyOr)).toBe(true); }); - it("exports filters correctly", () => { + it("export_filters", () => { const simpleParser = new MatchFilterParser("filesize>1000000 & duration<600"); const simpleExported = simpleParser.export(); expect(simpleExported).toEqual(["filesize>1000000&duration<600"]); @@ -177,12 +177,6 @@ describe("MatchFilterParser", () => { expect(normalize(complexExported)).toEqual(normalize(["filesize>1000000&duration<600", "uploader='BBC'"])); const testData = { filesize: 2000000, duration: 300 }; - for (const filter of complexExported) { - const filterParser = new MatchFilterParser(filter); - if (filterParser.evaluate(testData)) { - expect(true).toBe(true); - break; - } - } + expect(complexExported.some((filter) => new MatchFilterParser(filter).evaluate(testData))).toBe(true); }); });