added unit tests for other python3 modules
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import queue
|
||||
from src import app_log
|
||||
|
||||
|
||||
def test_log_enqueues_message_and_tag(monkeypatch):
|
||||
# Arrange
|
||||
q = queue.Queue()
|
||||
monkeypatch.setattr(app_log, "LOG_QUEUE", q)
|
||||
|
||||
# Act
|
||||
app_log.log("ready", "ok")
|
||||
|
||||
# Assert
|
||||
assert q.get_nowait() == ("ready", "ok")
|
||||
|
||||
|
||||
def test_log_uses_empty_tag_by_default(monkeypatch):
|
||||
# Arrange
|
||||
q = queue.Queue()
|
||||
monkeypatch.setattr(app_log, "LOG_QUEUE", q)
|
||||
|
||||
# Act
|
||||
app_log.log("ready")
|
||||
|
||||
# Assert
|
||||
assert q.get_nowait() == ("ready", "")
|
||||
@@ -0,0 +1,152 @@
|
||||
import math
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
import pytest
|
||||
from src import client
|
||||
|
||||
|
||||
def test_remove_wdb_does_nothing_when_directory_is_absent(tmp_path, monkeypatch):
|
||||
log = Mock()
|
||||
monkeypatch.setattr(client, "log", log)
|
||||
client.remove_wdb(str(tmp_path))
|
||||
log.assert_not_called()
|
||||
|
||||
|
||||
def test_remove_wdb_removes_existing_cache(tmp_path, monkeypatch):
|
||||
wdb = tmp_path / "WDB"
|
||||
wdb.mkdir()
|
||||
(wdb / "cache.bin").write_bytes(b"x")
|
||||
log = Mock()
|
||||
monkeypatch.setattr(client, "log", log)
|
||||
|
||||
client.remove_wdb(str(tmp_path))
|
||||
|
||||
assert not wdb.exists()
|
||||
log.assert_called_once_with("WDB cache cleared.", "dim")
|
||||
|
||||
|
||||
def test_remove_wdb_logs_failure_without_raising(tmp_path, monkeypatch):
|
||||
(tmp_path / "WDB").mkdir()
|
||||
monkeypatch.setattr(client.shutil, "rmtree", Mock(side_effect=PermissionError("locked")))
|
||||
log = Mock()
|
||||
monkeypatch.setattr(client, "log", log)
|
||||
|
||||
client.remove_wdb(str(tmp_path))
|
||||
|
||||
assert "locked" in log.call_args.args[0]
|
||||
assert log.call_args.args[1] == "err"
|
||||
|
||||
|
||||
def test_get_client_version_returns_empty_when_executable_is_missing(tmp_path):
|
||||
assert client.get_client_version(str(tmp_path)) == ""
|
||||
|
||||
|
||||
def test_get_client_version_reads_valid_version_and_build(tmp_path):
|
||||
exe = tmp_path / "WoW.exe"
|
||||
data = bytearray(0x00437c0a)
|
||||
data[0x00437bfc:0x00437c00] = b"5875"
|
||||
data[0x00437c04:0x00437c0a] = b"1.12.1"
|
||||
exe.write_bytes(data)
|
||||
|
||||
assert client.get_client_version(str(tmp_path)) == "1.12.1 (5875)"
|
||||
|
||||
|
||||
def test_get_client_version_rejects_invalid_version_bytes(tmp_path):
|
||||
exe = tmp_path / "WoW.exe"
|
||||
data = bytearray(0x00437c0a)
|
||||
data[0x00437bfc:0x00437c00] = b"5875"
|
||||
data[0x00437c04:0x00437c0a] = b"broken"
|
||||
exe.write_bytes(data)
|
||||
|
||||
assert client.get_client_version(str(tmp_path)) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value", "expected"), [
|
||||
(0, "0 KB"), (1023, "0 KB"), (1024, "1 KB"),
|
||||
(1024**2, "1.0 MB"), (1024**3, "1.00 GB"),
|
||||
])
|
||||
def test_fmt_size_formats_boundaries(value, expected):
|
||||
assert client.fmt_size(value) == expected
|
||||
|
||||
|
||||
def test_fmt_size_truncates_gigabytes_instead_of_rounding_up():
|
||||
assert client.fmt_size(8.7998 * 1024**3) == "8.79 GB"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value", "expected"), [
|
||||
(0, "0 KB/s"), (1024, "1 KB/s"),
|
||||
(1024**2 - 1, "1024 KB/s"), (1024**2, "1.0 MB/s"),
|
||||
])
|
||||
def test_fmt_speed_formats_boundaries(value, expected):
|
||||
assert client.fmt_speed(value) == expected
|
||||
|
||||
|
||||
def test_locale_patches_defaults_unknown_locale_to_english():
|
||||
assert client.locale_patches("not-a-locale") == client.locale_patches("enUS")
|
||||
|
||||
|
||||
def test_locale_patches_renames_reused_slot_for_russian():
|
||||
patches = client.locale_patches("ruRU")
|
||||
assert patches[-1] == (client._LOCALE_NAME_OFFSET(5), b"ruRU")
|
||||
|
||||
|
||||
def test_update_config_wtf_creates_config_when_missing(tmp_path, monkeypatch):
|
||||
writer = Mock()
|
||||
monkeypatch.setattr(client, "write_config_wtf", writer)
|
||||
tweaks = {"farClip": 500}
|
||||
|
||||
client.update_config_wtf(str(tmp_path), tweaks)
|
||||
|
||||
writer.assert_called_once_with(str(tmp_path), tweaks)
|
||||
|
||||
|
||||
def test_update_config_wtf_updates_existing_values_and_preserves_unrelated_lines(tmp_path, monkeypatch):
|
||||
cfg_dir = tmp_path / "WTF"
|
||||
cfg_dir.mkdir()
|
||||
cfg = cfg_dir / "Config.wtf"
|
||||
cfg.write_text('SET farClip "100"\nSET untouched "yes"\n', encoding="utf-8")
|
||||
monkeypatch.setattr(client, "log", Mock())
|
||||
|
||||
client.update_config_wtf(str(tmp_path), {"farClip": 777, "cameraDistance": 50,
|
||||
"nameplateRange": 41, "fieldOfView": 110, "soundInBackground": True})
|
||||
|
||||
text = cfg.read_text(encoding="utf-8")
|
||||
assert 'SET farClip "777"' in text
|
||||
assert 'SET untouched "yes"' in text
|
||||
assert 'SET CameraDistanceMax "50"' in text
|
||||
|
||||
|
||||
def test_fov_default_for_display_uses_lowest_reference_for_narrow_display(monkeypatch):
|
||||
monkeypatch.setattr(client, "_get_display_info_safe", lambda: {"width": 1024, "height": 1000})
|
||||
assert client.fov_default_for_display() == 90
|
||||
|
||||
|
||||
def test_fov_default_for_display_uses_highest_reference_for_ultrawide(monkeypatch):
|
||||
monkeypatch.setattr(client, "_get_display_info_safe", lambda: {"width": 5120, "height": 1080})
|
||||
assert client.fov_default_for_display() == 180
|
||||
|
||||
|
||||
def test_fov_default_for_display_falls_back_to_16_9_when_display_lookup_fails(monkeypatch):
|
||||
monkeypatch.setattr(client, "_get_display_info_safe", Mock(side_effect=RuntimeError("display unavailable")))
|
||||
assert client.fov_default_for_display() == 110
|
||||
|
||||
|
||||
def test_load_tweaks_config_merges_stored_values_with_defaults(monkeypatch):
|
||||
monkeypatch.setattr(client, "load_config", lambda: {"tweaks": {"farClip": 999}})
|
||||
monkeypatch.setattr(client, "fov_default_for_display", lambda: 150)
|
||||
|
||||
result = client.load_tweaks_config()
|
||||
|
||||
assert result["farClip"] == 999
|
||||
assert result["fieldOfView"] == 150
|
||||
assert result["cameraDistance"] == client.TWEAKS_DEFAULTS["cameraDistance"]
|
||||
|
||||
|
||||
def test_save_tweaks_config_stores_values(monkeypatch):
|
||||
cfg = {}
|
||||
monkeypatch.setattr(client, "update_config", lambda fn: fn(cfg))
|
||||
values = {"farClip": 900}
|
||||
|
||||
client.save_tweaks_config(values)
|
||||
|
||||
assert cfg["tweaks"] == values
|
||||
@@ -0,0 +1,221 @@
|
||||
import errno
|
||||
import urllib.error
|
||||
import zipfile
|
||||
from unittest.mock import Mock
|
||||
import pytest
|
||||
from src import mods
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value", "expected"), [
|
||||
("v1.2.0", (1, 2, 0)), ("V2.5-beta3", (2, 53)),
|
||||
("", (0,)), ("release", (0,)),
|
||||
])
|
||||
def test_parse_version_extracts_numeric_components(value, expected):
|
||||
assert mods._parse_version(value) == expected
|
||||
|
||||
|
||||
def test_updater_update_available_returns_false_without_latest_tag():
|
||||
assert mods.updater_update_available("") is False
|
||||
|
||||
|
||||
def test_updater_update_available_zero_pads_equivalent_versions(monkeypatch):
|
||||
monkeypatch.setattr(mods, "UPDATER_VERSION", "1.2.0")
|
||||
assert mods.updater_update_available("v1.2") is False
|
||||
|
||||
|
||||
def test_updater_update_available_detects_newer_version(monkeypatch):
|
||||
monkeypatch.setattr(mods, "UPDATER_VERSION", "1.2.0")
|
||||
assert mods.updater_update_available("v1.2.1") is True
|
||||
|
||||
|
||||
def test_pick_asset_returns_first_matching_asset():
|
||||
assets = [{"name": "mod.txt"}, {"name": "mod.zip"}]
|
||||
assert mods._pick_asset(assets, "*.zip", None) == assets[1]
|
||||
|
||||
|
||||
def test_pick_asset_prefers_name_without_excluded_text():
|
||||
assets = [{"name": "mod-dxvk.zip"}, {"name": "mod.zip"}]
|
||||
assert mods._pick_asset(assets, "*.zip", "-dxvk") == assets[1]
|
||||
|
||||
|
||||
def test_pick_asset_returns_none_when_nothing_matches():
|
||||
assert mods._pick_asset([{"name": "readme.txt"}], "*.zip", None) is None
|
||||
|
||||
|
||||
def test_release_version_uses_tag_by_default():
|
||||
mod = {"source": {"kind": "github_release"}}
|
||||
assert mods._release_version(mod, {"tag_name": "v2.0"}) == "v2.0"
|
||||
|
||||
|
||||
def test_release_version_can_extract_version_from_asset_name():
|
||||
mod = {"source": {"version_from": "asset", "asset_pattern": "*.zip", "prefer_no": None}}
|
||||
rel = {"assets": [{"name": "SuperWoW-1.4.2.zip"}], "tag_name": "static"}
|
||||
assert mods._release_version(mod, rel) == "1.4.2"
|
||||
|
||||
|
||||
def test_slim_release_discards_unused_release_fields():
|
||||
rel = {"tag_name": "v1", "body": "large", "assets": [{"name": "x.zip", "size": 12,
|
||||
"browser_download_url": "https://example/x", "extra": "drop"}]}
|
||||
assert mods._slim_release(rel) == {"tag_name": "v1", "assets": [{"name": "x.zip", "size": 12,
|
||||
"browser_download_url": "https://example/x"}]}
|
||||
|
||||
|
||||
def test_add_dll_creates_dlls_file(tmp_path):
|
||||
mods.add_dll(str(tmp_path), "Test.dll")
|
||||
assert (tmp_path / "dlls.txt").read_text() == "Test.dll\n"
|
||||
|
||||
|
||||
def test_add_dll_does_not_add_case_insensitive_duplicate(tmp_path):
|
||||
path = tmp_path / "dlls.txt"
|
||||
path.write_text("Test.dll\n")
|
||||
mods.add_dll(str(tmp_path), "test.DLL")
|
||||
assert path.read_text() == "Test.dll\n"
|
||||
|
||||
|
||||
def test_remove_dll_removes_named_entry_case_insensitively(tmp_path):
|
||||
path = tmp_path / "dlls.txt"
|
||||
path.write_text("One.dll\nTwo.dll\n")
|
||||
mods.remove_dll(str(tmp_path), "ONE.DLL")
|
||||
assert path.read_text() == "Two.dll\n"
|
||||
|
||||
|
||||
def test_remove_dll_deletes_empty_dlls_file(tmp_path):
|
||||
path = tmp_path / "dlls.txt"
|
||||
path.write_text("One.dll\n")
|
||||
mods.remove_dll(str(tmp_path), "one.dll")
|
||||
assert not path.exists()
|
||||
|
||||
|
||||
def test_remove_dll_does_nothing_when_file_is_missing(tmp_path):
|
||||
mods.remove_dll(str(tmp_path), "missing.dll")
|
||||
assert not (tmp_path / "dlls.txt").exists()
|
||||
|
||||
|
||||
def test_mod_installed_files_present_requires_nonempty_installed_file_list(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(mods, "load_config", lambda: {"mods": {"m": {"installed_files": []}}})
|
||||
assert mods.mod_installed_files_present({"id": "m"}, str(tmp_path)) is False
|
||||
|
||||
|
||||
def test_mod_installed_files_present_returns_true_when_every_file_exists(tmp_path, monkeypatch):
|
||||
(tmp_path / "a.dll").write_text("")
|
||||
(tmp_path / "b.dll").write_text("")
|
||||
monkeypatch.setattr(mods, "load_config", lambda: {"mods": {"m": {"installed_files": ["a.dll", "b.dll"]}}})
|
||||
assert mods.mod_installed_files_present({"id": "m"}, str(tmp_path)) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["direct_file", "direct_tar"])
|
||||
def test_mod_supports_update_check_rejects_pinned_direct_sources(kind):
|
||||
assert mods.mod_supports_update_check({"source": {"kind": kind}}) is False
|
||||
|
||||
|
||||
def test_mod_update_available_requires_enabled_mod():
|
||||
mod = {"source": {"kind": "github_release"}}
|
||||
state = {"enabled": False, "installed_version": "1"}
|
||||
assert mods.mod_update_available(mod, state, {"latest_version": "2"}) is False
|
||||
|
||||
|
||||
def test_mod_update_available_respects_ignore_updates():
|
||||
mod = {"source": {"kind": "github_release"}}
|
||||
state = {"enabled": True, "ignore_updates": True, "installed_version": "1"}
|
||||
assert mods.mod_update_available(mod, state, {"latest_version": "2"}) is False
|
||||
|
||||
|
||||
def test_mod_update_available_detects_different_live_version():
|
||||
mod = {"source": {"kind": "github_release"}}
|
||||
state = {"enabled": True, "installed_version": "1"}
|
||||
assert mods.mod_update_available(mod, state, {"latest_version": "2"}) is True
|
||||
|
||||
|
||||
def test_same_git_repo_ignores_case_trailing_slash_and_dot_git():
|
||||
assert mods._same_git_repo("https://github.com/Owner/Repo.git/", "https://github.com/owner/repo") is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", [
|
||||
"https://github.com/owner/repo", "https://sub.gitlab.com/owner/repo",
|
||||
"https://codeberg.org/owner/repo",
|
||||
])
|
||||
def test_is_allowed_git_url_accepts_supported_https_hosts(url):
|
||||
assert mods.is_allowed_git_url(url) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", ["http://github.com/a/b", "https://example.com/a/b", "not a url"])
|
||||
def test_is_allowed_git_url_rejects_unsupported_urls(url):
|
||||
assert mods.is_allowed_git_url(url) is False
|
||||
|
||||
|
||||
def test_slim_addon_catalog_keeps_only_supported_fields():
|
||||
catalog = [{"name": "A", "git": "g", "branch": "main", "ref": None, "description": "d",
|
||||
"extra": 1, "toc": {"Title": "T", "Notes": "N", "Interface": "11200", "Other": "drop"}}]
|
||||
result = mods._slim_addon_catalog(catalog)
|
||||
assert result == [{"name": "A", "git": "g", "branch": "main", "ref": None, "description": "d",
|
||||
"toc": {"Title": "T", "Notes": "N", "Interface": "11200"}}]
|
||||
|
||||
|
||||
def test_read_toc_file_parses_metadata_and_ignores_other_lines(tmp_path):
|
||||
path = tmp_path / "Addon.toc"
|
||||
path.write_text("\ufeff## Title: My Addon\nfile.lua\n## Interface: 11200\n", encoding="utf-8")
|
||||
assert mods.read_toc_file(str(path)) == {"Title": "My Addon", "Interface": "11200"}
|
||||
|
||||
|
||||
def test_read_toc_file_returns_empty_dict_when_missing(tmp_path):
|
||||
assert mods.read_toc_file(str(tmp_path / "missing.toc")) == {}
|
||||
|
||||
|
||||
def test_parse_wow_colored_splits_colored_and_plain_segments():
|
||||
text = "plain |cffff0000red|r normal"
|
||||
assert mods.parse_wow_colored(text) == [("plain ", None), ("red", "#ff0000"), (" normal", None)]
|
||||
|
||||
|
||||
def test_strip_wow_colors_removes_color_escape_sequences():
|
||||
assert mods.strip_wow_colors("A |cff00ff00green|r B") == "A green B"
|
||||
|
||||
|
||||
def test_git_parts_parses_github_dot_git_url():
|
||||
kind, repo_url, owner, repo, api = mods._git_parts("https://github.com/Owner/Repo.git")
|
||||
assert (kind, repo_url, owner, repo) == ("github", "https://github.com/Owner/Repo", "Owner", "Repo")
|
||||
assert api == mods.GITHUB_API
|
||||
|
||||
|
||||
def test_git_parts_parses_prefixed_gitea_url():
|
||||
kind, repo_url, owner, repo, api = mods._git_parts("https://octowow.st/git/Microchip/octo-updater")
|
||||
assert kind == "gitea"
|
||||
assert repo_url == "https://octowow.st/git/Microchip/octo-updater"
|
||||
assert api == "https://octowow.st/git/api/v1"
|
||||
|
||||
|
||||
def test_git_parts_rejects_url_without_owner_and_repo():
|
||||
with pytest.raises(ValueError, match="Unsupported git URL"):
|
||||
mods._git_parts("https://github.com/onlyone")
|
||||
|
||||
|
||||
def test_describe_net_error_maps_http_403_to_rate_limit_message():
|
||||
e = urllib.error.HTTPError("https://api.github.com/x", 403, "Forbidden", {}, None)
|
||||
assert "rate limit" in mods._describe_net_error(e)
|
||||
|
||||
|
||||
def test_describe_net_error_maps_http_404_to_not_found_message():
|
||||
e = urllib.error.HTTPError("https://api.github.com/x", 404, "Not Found", {}, None)
|
||||
assert mods._describe_net_error(e) == "repository or branch not found"
|
||||
|
||||
|
||||
def test_describe_install_error_maps_antivirus_like_os_errors():
|
||||
e = OSError(errno.EACCES, "denied")
|
||||
assert "antivirus" in mods.describe_install_error(e).lower()
|
||||
|
||||
|
||||
def test_describe_install_error_maps_bad_zip_file():
|
||||
assert "corrupted" in mods.describe_install_error(zipfile.BadZipFile()).lower()
|
||||
|
||||
|
||||
def test_addon_cached_sha_returns_cached_value_without_network(monkeypatch):
|
||||
key = "https://github.com/a/b#main"
|
||||
monkeypatch.setattr(mods, "load_config", lambda: {"addon_sha_cache": {key: {"sha": "abc"}}})
|
||||
assert mods.addon_cached_sha("https://github.com/a/b", branch="main") == "abc"
|
||||
|
||||
|
||||
def test_addon_zip_url_builds_github_archive_url():
|
||||
assert mods.addon_zip_url("https://github.com/a/b", "abc") == "https://github.com/a/b/archive/abc.zip"
|
||||
|
||||
|
||||
def test_addon_zip_url_builds_gitlab_archive_url():
|
||||
assert mods.addon_zip_url("https://gitlab.com/a/b", "abc") == "https://gitlab.com/a/b/-/archive/abc/b-abc.zip"
|
||||
@@ -0,0 +1,62 @@
|
||||
import urllib.request
|
||||
from unittest.mock import Mock
|
||||
import pytest
|
||||
from src import net
|
||||
|
||||
|
||||
def test_check_url_accepts_https_when_no_allowlist():
|
||||
net._check_url("https://example.com/file", None)
|
||||
|
||||
|
||||
def test_check_url_rejects_http():
|
||||
with pytest.raises(RuntimeError, match="non-HTTPS"):
|
||||
net._check_url("http://example.com/file", None)
|
||||
|
||||
|
||||
def test_check_url_accepts_allowlisted_host_case_insensitively():
|
||||
net._check_url("https://EXAMPLE.COM/file", {"example.com"})
|
||||
|
||||
|
||||
def test_check_url_rejects_host_not_in_allowlist():
|
||||
with pytest.raises(RuntimeError, match="unexpected host"):
|
||||
net._check_url("https://evil.example/file", {"example.com"})
|
||||
|
||||
|
||||
def test_check_url_does_not_treat_subdomain_as_exact_allowlist_match():
|
||||
with pytest.raises(RuntimeError, match="unexpected host"):
|
||||
net._check_url("https://cdn.example.com/file", {"example.com"})
|
||||
|
||||
|
||||
def test_secure_urlopen_opens_valid_url_with_timeout(monkeypatch):
|
||||
# Arrange
|
||||
response = object()
|
||||
opener = Mock()
|
||||
opener.open.return_value = response
|
||||
monkeypatch.setattr(net, "_SECURE_OPENER", opener)
|
||||
|
||||
# Act
|
||||
result = net.secure_urlopen("https://example.com/file", 7, {"example.com"})
|
||||
|
||||
# Assert
|
||||
assert result is response
|
||||
opener.open.assert_called_once_with("https://example.com/file", timeout=7)
|
||||
|
||||
|
||||
def test_secure_urlopen_validates_request_full_url_before_opening(monkeypatch):
|
||||
# Arrange
|
||||
opener = Mock()
|
||||
monkeypatch.setattr(net, "_SECURE_OPENER", opener)
|
||||
req = urllib.request.Request("http://example.com/file")
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(RuntimeError, match="non-HTTPS"):
|
||||
net.secure_urlopen(req, 5)
|
||||
opener.open.assert_not_called()
|
||||
|
||||
|
||||
def test_redirect_handler_rejects_https_to_http_downgrade():
|
||||
handler = net._HttpsOnlyRedirectHandler()
|
||||
req = urllib.request.Request("https://example.com/start")
|
||||
|
||||
with pytest.raises(RuntimeError, match="non-HTTPS"):
|
||||
handler.redirect_request(req, None, 302, "Found", {}, "http://example.com/end")
|
||||
@@ -0,0 +1,15 @@
|
||||
from unittest.mock import Mock
|
||||
import octo_updater
|
||||
|
||||
|
||||
def test_main_prepares_platform_and_data_before_starting_ui(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(octo_updater, "enable_dpi_awareness", lambda: calls.append("dpi"))
|
||||
monkeypatch.setattr(octo_updater, "_relocate_legacy_data", lambda: calls.append("relocate"))
|
||||
app = Mock()
|
||||
monkeypatch.setattr(octo_updater, "OctoUpdaterApp", lambda: calls.append("construct") or app)
|
||||
|
||||
octo_updater.main()
|
||||
|
||||
assert calls == ["dpi", "relocate", "construct"]
|
||||
app.mainloop.assert_called_once_with()
|
||||
@@ -0,0 +1,58 @@
|
||||
import io
|
||||
import json
|
||||
from unittest.mock import Mock
|
||||
from src import ui
|
||||
|
||||
|
||||
class JsonResponse(io.BytesIO):
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *args): return False
|
||||
|
||||
|
||||
def test_strip_html_removes_tags_scripts_and_decodes_entities():
|
||||
raw = '<p>Hello & goodbye</p><script>bad()</script><br><b>World</b>'
|
||||
assert ui._strip_html(raw) == "Hello & goodbye\n\nWorld"
|
||||
|
||||
|
||||
def test_strip_html_formats_list_items_as_bullets():
|
||||
assert ui._strip_html("<ul><li>One</li><li>Two</li></ul>") == "• One\n\n• Two"
|
||||
|
||||
|
||||
def test_format_news_date_formats_valid_iso_date():
|
||||
assert ui._format_news_date("2026-09-27T12:30:00+00:00") == "27 Sep 2026"
|
||||
|
||||
|
||||
def test_format_news_date_returns_original_invalid_value():
|
||||
assert ui._format_news_date("not-a-date") == "not-a-date"
|
||||
|
||||
|
||||
def test_fetch_patch_notes_returns_items_newest_first(monkeypatch):
|
||||
payload = {"items": [{"id": 1, "date": "2026-01-01"}, {"id": 2, "date": "2026-09-01"}]}
|
||||
opener = Mock(return_value=JsonResponse(json.dumps(payload).encode()))
|
||||
monkeypatch.setattr(ui, "secure_urlopen", opener)
|
||||
|
||||
result = ui.fetch_patch_notes()
|
||||
|
||||
assert [x["id"] for x in result] == [2, 1]
|
||||
assert opener.call_args.kwargs["timeout"] == ui.NEWS_TIMEOUT
|
||||
|
||||
|
||||
def test_fetch_patch_notes_returns_empty_list_when_items_missing(monkeypatch):
|
||||
monkeypatch.setattr(ui, "secure_urlopen", Mock(return_value=JsonResponse(b"{}")))
|
||||
assert ui.fetch_patch_notes() == []
|
||||
|
||||
|
||||
def test_fetch_featured_post_returns_valid_post(monkeypatch):
|
||||
payload = {"id": 7, "title": "News"}
|
||||
monkeypatch.setattr(ui, "secure_urlopen", Mock(return_value=JsonResponse(json.dumps(payload).encode())))
|
||||
assert ui.fetch_featured_post() == payload
|
||||
|
||||
|
||||
def test_fetch_featured_post_returns_none_when_id_missing(monkeypatch):
|
||||
monkeypatch.setattr(ui, "secure_urlopen", Mock(return_value=JsonResponse(b'{"title":"News"}')))
|
||||
assert ui.fetch_featured_post() is None
|
||||
|
||||
|
||||
def test_fetch_featured_post_returns_none_for_non_object_json(monkeypatch):
|
||||
monkeypatch.setattr(ui, "secure_urlopen", Mock(return_value=JsonResponse(b'[]')))
|
||||
assert ui.fetch_featured_post() is None
|
||||
Reference in New Issue
Block a user