"""Install-link updates: a manifest's `version`, remembered with the game and checked again at start.""" import json import pytest from frameport import deeplink, pipeline from frameport.core import library APK = "https://cdn.example.com/game-arm64.apk" SRC = "https://cdn.example.com/game.json" def _data(**kw): return {"schema": deeplink.SCHEMA, "name": "G", "files": [{"url": APK}], **kw} @pytest.mark.parametrize("data,version", [ (_data(version="1.4.2"), "1.4.2"), (_data(version=3), "3"), (_data(frameport={"version": " 2.0 "}), "2.0"), (_data(version="1.0", frameport={"version": "2.0"}), "2.0"), (_data(version=True), ""), (_data(version={"x": 1}), ""), (_data(version="x" * 100), ""), (_data(), ""), ]) def test_manifest_version(data, version): assert deeplink.manifest_from_data(data, SRC).version == version @pytest.mark.parametrize("a,b", [ ("1.10", "1.9"), ("v2", "1.99.99"), ("1.2.0", "1.2.0-beta3"), ("1.2.0-rc10", "1.2.0-rc2"), ("1.2.1", "1.2"), ("1.0.1", "1.0.0b"), ]) def test_newer_versions(a, b): assert deeplink.compare_versions(a, b) == 1 and deeplink.compare_versions(b, a) == -1 @pytest.mark.parametrize("a,b", [("1.2", "1.2.0"), ("v1.2", "1.2"), ("build 7", "build 7")]) def test_equal_versions(a, b): assert deeplink.compare_versions(a, b) == 0 def test_mixed_versions_dont_crash(): assert deeplink.compare_versions("1.0-2", "1.0-b") in (-1, 1) assert deeplink.compare_versions("nightly", "1.0") in (-1, 1) assert not deeplink.is_newer_version("2.0", "") and not deeplink.is_newer_version("", "1.0") @pytest.mark.parametrize("link,ok", [ ({"source": SRC, "version": "1.0"}, True), ({"source": SRC, "version": ""}, False), ({"source": SRC, "version": "1.0", "direct": True}, False), ({"source": APK, "version": "1.0"}, False), # a bare file link ({"source": "/home/u/game.json", "version": "1.0"}, False), ({"source": deeplink.DEMO_MANIFEST, "version": "1.0"}, False), ({"source": "https://10.0.0.2/game.json", "version": "1.0"}, False), (None, False), ]) def test_which_links_are_checked(link, ok): assert deeplink.can_check_updates(link) is ok def test_links_without_a_version_use_the_installed_one(): """A manifest that named no version (e.g. a first release) is still checked: against the installed APK's own versionName. The game page's "Check for updates" needs only a manifest it can fetch again.""" link = {"source": SRC, "version": ""} assert not deeplink.can_check_updates(link) and deeplink.can_check_updates(link, "0.1.0") assert deeplink.can_check_link(link) and not deeplink.can_check_link({"source": APK, "version": ""}) assert not deeplink.can_check_link({"source": SRC, "direct": True}) g = {"package": "com.x", "apk": "/x.apk", "link": link, "analysis": {"version": "0.1.0"}} assert pipeline.link_installed_version(g) == "0.1.0" assert pipeline.link_installed_version({**g, "link": {"source": SRC, "version": "0.2"}}) == "0.2" assert pipeline.link_installed_version({**g, "kind": "linux"}) == "" def test_check_finds_an_update_for_a_link_without_a_version(monkeypatch, tmp_path): library.upsert_game("com.x", title="X", apk=str(tmp_path / "x.apk"), analysis={"version": "0.1.0"}, link={"source": SRC, "name": "X", "version": "", "direct": False}) monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data( _data(version="0.2.1"), req.manifest_url)) assert pipeline.check_link_updates(force=True) == ["com.x"] assert library.game("com.x")["link_update"]["current"] == "0.1.0" def _add(monkeypatch, tmp_path, version, name="G"): path = tmp_path / "Tool.exe" path.write_bytes(b"MZ") if not library.game("rift.tool"): library.upsert_game("rift.tool", kind="rift", details={"sources": []}) monkeypatch.setattr(pipeline, "add_windows_exe", lambda p, r=None: library.game("rift.tool")) m = deeplink.manifest_from_data(_data(name=name, version=version, files=[{"url": "https://c.example.com/T.exe"}]), SRC) return pipeline.add_from_link(m, path) def test_check_marks_and_clears_updates(monkeypatch, tmp_path): g = _add(monkeypatch, tmp_path, "1.0") assert g["link"]["version"] == "1.0" and g["link"]["direct"] is False monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data( _data(version="1.1"), req.manifest_url)) assert pipeline.check_link_updates(force=True) == ["rift.tool"] assert library.game("rift.tool")["link_update"]["version"] == "1.1" # within LINK_CHECK_HOURS nothing is fetched, the known offer stays monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("fetched too early")) assert pipeline.check_link_updates() == ["rift.tool"] # installing the update records its version and clears the offer g = _add(monkeypatch, tmp_path, "1.1") assert g["link"]["version"] == "1.1" and "link_update" not in g # an offer the manifest withdrew goes away library.upsert_game("rift.tool", link_update={"version": "9"}) monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data(_data(version="1.1"), SRC)) assert pipeline.check_link_updates(force=True) == [] # a manifest that can't be read keeps the state def offline(req): raise OSError("offline") library.upsert_game("rift.tool", link_update={"version": "1.2"}) monkeypatch.setattr(deeplink, "fetch_manifest", offline) assert pipeline.check_link_updates(force=True) == ["rift.tool"] def test_check_can_be_switched_off(monkeypatch, tmp_path): _add(monkeypatch, tmp_path, "1.0") library.set_setting("links.check_updates", False) monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("checked while off")) assert pipeline.check_link_updates() == [] def test_update_keeps_the_users_own_name(monkeypatch, tmp_path): _add(monkeypatch, tmp_path, "1.0", name="Tool") library.upsert_game("rift.tool", title="My Tool", title_locked=True) assert _add(monkeypatch, tmp_path, "1.1", name="Tool")["title"] == "My Tool" library.upsert_game("rift.tool", title="Tool") # not renamed: a new name from the manifest is taken assert _add(monkeypatch, tmp_path, "1.2", name="Tool 2")["title"] == "Tool 2" def test_demo_link_is_never_checked_for_updates(monkeypatch): """The homepage's example button stays a demo: its manifest names a version, but nothing about it is fetched.""" from pathlib import Path site = json.loads((Path(__file__).parents[1] / "site/public/demo/cool-game.json").read_text()) assert deeplink.manifest_from_data(site, deeplink.DEMO_MANIFEST).version == deeplink.demo_manifest().version assert deeplink.parse(deeplink.make_link(deeplink.DEMO_MANIFEST)).demo assert not deeplink.can_check_updates({"source": deeplink.DEMO_MANIFEST, "version": "0.1"}) library.upsert_game("demo.like", link={"source": deeplink.DEMO_MANIFEST, "version": "0.1"}) monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("the demo was fetched")) assert pipeline.check_link_updates(force=True) == [] def test_local_server_round_trip(tmp_path): """check_update fetches the remembered manifest over HTTP (loopback is allowed for testing).""" import threading from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer class Quiet(SimpleHTTPRequestHandler): def log_message(self, *args): pass (tmp_path / "game.json").write_text(json.dumps(_data(version="2.0"))) httpd = ThreadingHTTPServer(("127.0.0.1", 0), partial(Quiet, directory=str(tmp_path))) threading.Thread(target=httpd.serve_forever, daemon=True).start() try: link = {"source": f"http://127.0.0.1:{httpd.server_address[1]}/game.json", "version": "1.0"} assert deeplink.can_check_updates(link) assert deeplink.check_update(link).version == "2.0" assert deeplink.check_update({**link, "version": "2.0"}) is None finally: httpd.shutdown() def test_update_toast_has_an_update_button(monkeypatch): """The toast that announces a newer version starts the update itself (it used to offer no way to get it).""" from frameport.ui import app as app_mod from frameport.ui.views import link_dialog toasts, started = [], [] a = app_mod.FramePortApp.__new__(app_mod.FramePortApp) a.route = ("library",) monkeypatch.setattr(a, "refresh_view", lambda: None, raising=False) monkeypatch.setattr(a, "_title", lambda pkg: pkg, raising=False) monkeypatch.setattr(a, "toast", lambda msg, **k: toasts.append(k), raising=False) monkeypatch.setattr(library, "games", lambda: []) monkeypatch.setattr(pipeline, "check_link_updates", lambda force=False: ["linux.tool"]) monkeypatch.setattr(link_dialog, "update_from_link", lambda app, pkgs: started.append(pkgs)) a._check_link_updates() assert toasts and toasts[0]["action"] toasts[0]["on_action"](None) assert started == [["linux.tool"]]