"""Tests for the GitHub Releases update checker.""" import logging from unittest.mock import MagicMock, patch from vocalinux.utils.update_checker import ( ReleaseInfo, build_channel, fetch_latest_release, format_release_notes, is_newer_version, is_trusted_release_url, is_update_available, normalize_channel, normalize_version, ) class TestNormalizeAndCompare: def test_normalize_version_strips_v_prefix(self): assert normalize_version("v0.15.0") == "0.15.0" assert normalize_version("V0.15.0") == "0.15.0" def test_equal_versions_are_not_newer(self): assert not is_newer_version("0.15.0", "0.15.0") assert not is_newer_version("v0.15.0", "0.15.0") def test_numeric_ordering(self): assert is_newer_version("0.9.0", "0.10.0") assert is_newer_version("0.15.0", "0.15.1") assert not is_newer_version("1.0.0", "0.99.0") def test_stable_outranks_prerelease(self): assert is_newer_version("0.15.0-beta", "0.15.0") assert not is_newer_version("0.15.0", "0.15.0-beta") def test_dot_prerelease_suffix(self): assert is_newer_version("0.15.0.rc1", "0.15.0") assert not is_newer_version("0.15.0", "0.15.0.rc1") def test_build_metadata_ignored(self): assert not is_newer_version("0.15.0+flatpak", "0.15.0") class TestChannels: def test_build_channel_from_version(self) -> None: cases = ( ("0.18.3", "stable"), ("v0.18.3", "stable"), ("0.18.3+flatpak", "stable"), ("0.14.0-beta", "stable"), ("0.18.3.dev20261020+4799f06", "nightly"), ("0.18.3.dev20261020", "nightly"), ) for version, expected in cases: assert build_channel(version) == expected def test_build_channel_reads_version_module_at_call_time(self) -> None: with patch( "vocalinux.version.__version__", "0.18.3.dev20261020+4799f06", ): assert build_channel() == "nightly" def test_normalize_channel(self): assert normalize_channel("stable") == "stable" assert normalize_channel("NIGHTLY") == "nightly" assert normalize_channel("nope") == "stable" def test_stable_update_uses_semver(self): release = ReleaseInfo( tag_name="v0.15.0", version="0.15.0", name="v0.15.0", html_url="https://github.com/VocaHQ/vocalinux/releases/tag/v0.15.0", body="", published_at="2026-08-02T00:00:00Z", prerelease=False, channel="stable", ) assert is_update_available("0.14.0", release, "stable") assert not is_update_available("0.15.0", release, "stable") def test_nightly_update_compares_dates(self): release = ReleaseInfo( tag_name="nightly-2026-08-02", version="nightly-2026-08-02", name="Nightly Build - 0.14.2.dev20260802+27ecf88", html_url="https://github.com/VocaHQ/vocalinux/releases/tag/nightly-2026-08-02", body="", published_at="2026-08-02T00:00:00Z", prerelease=True, channel="nightly", ) assert is_update_available("nightly-2026-08-01", release, "nightly") assert not is_update_available("nightly-2026-08-02", release, "nightly") # Installed nightlies use .devYYYYMMDD[+sha], not the GitHub tag name. assert not is_update_available("0.14.2.dev20260802+27ecf88", release, "nightly") assert is_update_available("0.14.2.dev20260801+aaaaaaa", release, "nightly") # Stable installs following nightly should be offered the latest nightly. assert is_update_available("0.15.0", release, "nightly") class TestTrustedUrls: def test_accepts_project_urls(self): assert is_trusted_release_url("https://github.com/VocaHQ/vocalinux") assert is_trusted_release_url("https://github.com/VocaHQ/vocalinux/releases/tag/v0.15.0") # Pre-transfer URLs still redirect and must stay trusted. assert is_trusted_release_url("https://github.com/jatinkrmalik/vocalinux") assert is_trusted_release_url( "https://github.com/jatinkrmalik/vocalinux/releases/tag/v0.15.0" ) def test_rejects_untrusted_urls(self): assert not is_trusted_release_url("https://evil.example/payload") assert not is_trusted_release_url("http://github.com/VocaHQ/vocalinux") assert not is_trusted_release_url("https://github.com/evil/vocalinux") assert not is_trusted_release_url("") class TestFormatReleaseNotes: def test_empty_body(self): assert "No release notes" in format_release_notes("") def test_keeps_code_block_contents(self): body = "Install with:\n```bash\ncurl -fsSL https://example.com | bash\n```\nDone" notes = format_release_notes(body) assert "curl -fsSL https://example.com | bash" in notes assert "```" not in notes def test_strips_images_and_keeps_links(self): body = 'See [docs](https://example.com) x' notes = format_release_notes(body) assert "docs (https://example.com)" in notes assert "= logging.WARNING ]