This repository was archived by the owner on May 2, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 77
Fix: failing test + add unit tests for core features + re-enable test step in Actions #201
Open
Type-Delta
wants to merge
7
commits into
master
Choose a base branch
from
fix/test
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e9c6841
fix(test): assertion string missmatch + mock webbrowser.open
Type-Delta 6813dc9
test: Add comprehensive test coverage across modules
Type-Delta 5be6f35
ci(test): uncomment test step
Type-Delta 7c5a856
fix(types): termios type errors on windows
Type-Delta 885f35a
fix(test): AuthService lock file pollution
Type-Delta 9cc322d
fix(icat): move termios, tty modules behind os gate to prevent import…
Type-Delta 3e207fc
ci(test): add caching to apt-get install step
Type-Delta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
tests/cli/commands/anilist/commands/test_notifications.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| from contextlib import nullcontext | ||
| from datetime import datetime | ||
| from types import SimpleNamespace | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| from click.testing import CliRunner | ||
|
|
||
| from viu_media.cli.commands.anilist.commands.notifications import notifications | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def runner(): | ||
| return CliRunner() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_config(): | ||
| return SimpleNamespace(general=SimpleNamespace(media_api="anilist")) | ||
|
|
||
|
|
||
| def test_notifications_requires_authentication(runner, mock_config): | ||
| with ( | ||
| patch("viu_media.cli.service.feedback.FeedbackService") as mock_feedback, | ||
| patch("viu_media.libs.media_api.api.create_api_client") as mock_create_api, | ||
| patch("viu_media.cli.service.auth.AuthService") as mock_auth, | ||
| ): | ||
| feedback_instance = mock_feedback.return_value | ||
| feedback_instance.progress.return_value = nullcontext() | ||
|
|
||
| auth_instance = mock_auth.return_value | ||
| auth_instance.get_auth.return_value = None | ||
|
|
||
| api_client = mock_create_api.return_value | ||
| api_client.is_authenticated.return_value = False | ||
|
|
||
| result = runner.invoke(notifications, [], obj=mock_config) | ||
|
|
||
| assert result.exit_code == 0 | ||
| feedback_instance.error.assert_called_with( | ||
| "Authentication Required", "Please log in with 'viu anilist auth'." | ||
| ) | ||
|
|
||
|
|
||
| def test_notifications_shows_all_caught_up_message(runner, mock_config): | ||
| with ( | ||
| patch("viu_media.cli.service.feedback.FeedbackService") as mock_feedback, | ||
| patch("viu_media.libs.media_api.api.create_api_client") as mock_create_api, | ||
| patch("viu_media.cli.service.auth.AuthService") as mock_auth, | ||
| ): | ||
| feedback_instance = mock_feedback.return_value | ||
| feedback_instance.progress.return_value = nullcontext() | ||
|
|
||
| auth_instance = mock_auth.return_value | ||
| auth_instance.get_auth.return_value = SimpleNamespace(token="token") | ||
|
|
||
| api_client = mock_create_api.return_value | ||
| api_client.is_authenticated.return_value = True | ||
| api_client.get_notifications.return_value = [] | ||
|
|
||
| result = runner.invoke(notifications, [], obj=mock_config) | ||
|
|
||
| assert result.exit_code == 0 | ||
| api_client.authenticate.assert_called_once_with("token") | ||
| feedback_instance.success.assert_called_with( | ||
| "All caught up!", "You have no new notifications." | ||
| ) | ||
|
|
||
|
|
||
| def test_notifications_prints_table_and_mark_read_info(runner, mock_config): | ||
| notification_item = SimpleNamespace( | ||
| created_at=datetime(2025, 1, 1, 10, 0, 0), | ||
| media=SimpleNamespace(title=SimpleNamespace(english="Blue Lock", romaji=None)), | ||
| episode=12, | ||
| ) | ||
|
|
||
| with ( | ||
| patch("viu_media.cli.service.feedback.FeedbackService") as mock_feedback, | ||
| patch("viu_media.libs.media_api.api.create_api_client") as mock_create_api, | ||
| patch("viu_media.cli.service.auth.AuthService") as mock_auth, | ||
| patch("viu_media.cli.commands.anilist.commands.notifications.Console") as mock_console, | ||
| ): | ||
| feedback_instance = mock_feedback.return_value | ||
| feedback_instance.progress.return_value = nullcontext() | ||
|
|
||
| auth_instance = mock_auth.return_value | ||
| auth_instance.get_auth.return_value = SimpleNamespace(token="token") | ||
|
|
||
| api_client = mock_create_api.return_value | ||
| api_client.is_authenticated.return_value = True | ||
| api_client.get_notifications.return_value = [notification_item] | ||
|
|
||
| console_instance = mock_console.return_value | ||
|
|
||
| result = runner.invoke(notifications, [], obj=mock_config) | ||
|
|
||
| assert result.exit_code == 0 | ||
| console_instance.print.assert_called_once() | ||
| feedback_instance.info.assert_called_once_with( | ||
| "Notifications have been marked as read on AniList.", | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from viu_media.cli.service.auth.service import AuthService | ||
| from viu_media.libs.media_api.types import UserProfile | ||
|
|
||
|
|
||
| def test_load_auth_creates_file_when_missing(tmp_path, monkeypatch): | ||
| auth_file = tmp_path / "auth.json" | ||
| monkeypatch.setattr("viu_media.cli.service.auth.service.AUTH_FILE", auth_file) | ||
|
|
||
| service = AuthService(media_api="anilist") | ||
| profile = service.get_auth() | ||
|
|
||
| assert profile is None | ||
| assert auth_file.exists() | ||
|
|
||
|
|
||
| def test_save_and_get_auth_roundtrip(tmp_path, monkeypatch): | ||
| auth_file = tmp_path / "auth.json" | ||
| monkeypatch.setattr("viu_media.cli.service.auth.service.AUTH_FILE", auth_file) | ||
|
|
||
| service = AuthService(media_api="anilist") | ||
| user = UserProfile(id=1, name="test-user", avatar_url="https://img/avatar.png") | ||
|
|
||
| service.save_user_profile(user, "token-abc") | ||
| auth = service.get_auth() | ||
|
|
||
| assert auth is not None | ||
| assert auth.token == "token-abc" | ||
| assert auth.user_profile.name == "test-user" | ||
|
|
||
|
|
||
| def test_clear_user_profile_deletes_auth_file(tmp_path, monkeypatch): | ||
| auth_file = tmp_path / "auth.json" | ||
| monkeypatch.setattr("viu_media.cli.service.auth.service.AUTH_FILE", auth_file) | ||
|
|
||
| service = AuthService(media_api="anilist") | ||
| user = UserProfile(id=2, name="clear-me") | ||
| service.save_user_profile(user, "token") | ||
| assert auth_file.exists() | ||
|
|
||
| service.clear_user_profile() | ||
|
|
||
| assert not auth_file.exists() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import os | ||
| import time | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from viu_media.core.utils.file import AtomicWriter, FileLock, check_file_modified, sanitize_filename | ||
|
|
||
|
|
||
| def test_atomic_writer_writes_atomically(tmp_path: Path): | ||
| target = tmp_path / "data.txt" | ||
|
|
||
| with AtomicWriter(target, mode="w", encoding="utf-8") as handle: | ||
| handle.write("hello world") | ||
|
|
||
| assert target.read_text(encoding="utf-8") == "hello world" | ||
|
|
||
|
|
||
| def test_atomic_writer_cleans_temp_file_on_exception(tmp_path: Path): | ||
| target = tmp_path / "data.txt" | ||
| target.write_text("original", encoding="utf-8") | ||
|
|
||
| with pytest.raises(RuntimeError, match="boom"): | ||
| with AtomicWriter(target, mode="w", encoding="utf-8") as handle: | ||
| handle.write("new") | ||
| raise RuntimeError("boom") | ||
|
|
||
| assert target.read_text(encoding="utf-8") == "original" | ||
| assert list(tmp_path.glob("*.tmp")) == [] | ||
|
|
||
|
|
||
| def test_file_lock_acquire_release_non_blocking(tmp_path: Path): | ||
| lock_path = tmp_path / "my.lock" | ||
| lock = FileLock(lock_path, timeout=0, stale_timeout=10) | ||
|
|
||
| lock.acquire() | ||
| assert lock_path.exists() | ||
|
|
||
| lock.release() | ||
| assert not lock_path.exists() | ||
|
|
||
|
|
||
| def test_file_lock_breaks_stale_lock(tmp_path: Path): | ||
| lock_path = tmp_path / "stale.lock" | ||
| lock_path.write_text("999999\n0", encoding="utf-8") | ||
| stale_timestamp = time.time() - 100 | ||
| os.utime(lock_path, (stale_timestamp, stale_timestamp)) | ||
|
|
||
| lock = FileLock(lock_path, timeout=0.5, stale_timeout=0.01) | ||
| lock.acquire() | ||
|
|
||
| assert lock_path.exists() | ||
| lock.release() | ||
| assert not lock_path.exists() | ||
|
|
||
|
|
||
| def test_check_file_modified_detects_changes(tmp_path: Path): | ||
| target = tmp_path / "track.txt" | ||
| target.write_text("a", encoding="utf-8") | ||
| previous_mtime = target.stat().st_mtime | ||
|
|
||
| time.sleep(0.01) | ||
| target.write_text("b", encoding="utf-8") | ||
|
|
||
| current_mtime, modified = check_file_modified(target, previous_mtime) | ||
|
|
||
| assert modified is True | ||
| assert current_mtime > previous_mtime | ||
|
|
||
|
|
||
| def test_sanitize_filename_removes_invalid_characters(): | ||
| result = sanitize_filename('My:/Invalid*"Name?', restricted=False) | ||
|
|
||
| assert "?" not in result | ||
| assert "*" not in result | ||
| assert result |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| from unittest.mock import patch | ||
|
|
||
| import httpx | ||
|
|
||
| from viu_media.core.utils.networking import get_remote_filename, random_user_agent | ||
|
|
||
|
|
||
| def _make_response(url: str, headers: dict[str, str] | None = None) -> httpx.Response: | ||
| request = httpx.Request("GET", url) | ||
| return httpx.Response(200, headers=headers, request=request) | ||
|
|
||
|
|
||
| def test_random_user_agent_uses_selected_chrome_version(): | ||
| with patch("viu_media.core.utils.networking.random.choice", return_value="97.0.4692.20"): | ||
| user_agent = random_user_agent() | ||
|
|
||
| assert "Chrome/97.0.4692.20" in user_agent | ||
| assert user_agent.startswith("Mozilla/5.0") | ||
|
|
||
|
|
||
| def test_get_remote_filename_prefers_filename_star_header(): | ||
| response = _make_response( | ||
| "https://example.com/download", | ||
| {"Content-Disposition": "attachment; filename*=UTF-8''my%20anime%20file.mkv"}, | ||
| ) | ||
|
|
||
| filename = get_remote_filename(response) | ||
|
|
||
| assert filename == "my anime file.mkv" | ||
|
|
||
|
|
||
| def test_get_remote_filename_uses_regular_filename_header(): | ||
| response = _make_response( | ||
| "https://example.com/download", | ||
| {"Content-Disposition": 'attachment; filename="episode-01.mp4"'}, | ||
| ) | ||
|
|
||
| filename = get_remote_filename(response) | ||
|
|
||
| assert filename == "episode-01.mp4" | ||
|
|
||
|
|
||
| def test_get_remote_filename_falls_back_to_url_path(): | ||
| response = _make_response("https://example.com/files/ep%2001.mp4?token=abc") | ||
|
|
||
| filename = get_remote_filename(response) | ||
|
|
||
| assert filename == "ep 01.mp4" | ||
|
|
||
|
|
||
| def test_get_remote_filename_returns_none_when_no_candidate_found(): | ||
| response = _make_response("https://example.com/") | ||
|
|
||
| filename = get_remote_filename(response) | ||
|
|
||
| assert filename is None |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.