diff --git a/CHANGELOG.md b/CHANGELOG.md index 47765fa..3efa51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ -# v0.8.30-beta — In ontwikkeling +# v0.8.30-beta — De updater neemt genoegen met het adres uit je adresbalk -Nog niets uitgebracht sinds v0.8.29. Wat hier onder komt te staan gaat mee in -de volgende uitgave; bij het uitbrengen wordt deze kop de releaseversie. +Wie in Forgejo naar zijn releases keek en dat adres in de instellingen plakte, +kreeg een webpagina in plaats van JSON — en als enige melding +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`. Daar leidt +niemand uit af dat er `/api/v1/repos/` in het adres had moeten staan. + +- **Het adres wordt omgerekend.** `https://git.example.nl/bes-r/server-up` en + `.../releases` worden allebei het juiste API-adres; GitHub krijgt zijn eigen + `api.github.com`. Een adres dat al klopt of dat we niet herkennen blijft staan. +- **Een begrijpelijke melding** als er tóch HTML terugkomt, met wat er dan wél + in het veld hoort. +- **`https://` voor het imageveld wordt weggehaald.** Een image-adres heeft + nooit een schema, dus `docker pull https://…` faalde op iets wat er in het + veld goed uitzag. # v0.8.29 — UniFi start weer op een Raspberry Pi 4 diff --git a/server-up/core/selfupdate.py b/server-up/core/selfupdate.py index 501849a..27a1ba7 100644 --- a/server-up/core/selfupdate.py +++ b/server-up/core/selfupdate.py @@ -82,6 +82,15 @@ def image_repo(info: dict | None = None) -> str: """ expliciet = (cfg.load().get("UPDATE_IMAGE") or "").strip() if expliciet: + # Een image-adres heeft nooit een schema. Wie het uit zijn adresbalk + # plakt neemt `https://` mee, en dan faalt `docker pull` op iets wat er + # in het veld goed uitziet. Weghalen is hier eenduidig: er bestaat geen + # geldig image dat zo begint. + for schema in ("https://", "http://"): + if expliciet.lower().startswith(schema): + expliciet = expliciet[len(schema):] + break + expliciet = expliciet.strip("/") return expliciet.rsplit(":", 1)[0] if _has_tag(expliciet) else expliciet img = (info or inspect_self()).get("image", "") if not img: diff --git a/server-up/core/updater.py b/server-up/core/updater.py index 09b378e..2886b3b 100644 --- a/server-up/core/updater.py +++ b/server-up/core/updater.py @@ -78,11 +78,46 @@ def image_tag(release_tag: str) -> str: return tag[1:] if tag.startswith("v") and _parse(tag) else tag +def api_url(url: str) -> str: + """Maak van een geplakt adres het API-adres. + + Wie in Forgejo of GitHub naar zijn releases kijkt en dat adres uit de + adresbalk plakt, geeft het adres van een wébpagina op. Dat levert HTML op, + en de enige melding die je daarna kreeg was `JSONDecodeError: Expecting + value: line 1 column 1` — waaruit niemand opmaakt dat er `/api/v1/` had + moeten staan. Dus rekenen we het om in plaats van erover te vallen. + + https://git.voorbeeld.nl/bes-r/server-up/releases + → https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases + https://github.com/bes-r/server-up/releases + → https://api.github.com/repos/bes-r/server-up/releases + """ + schoon = (url or "").strip().rstrip("/") + if not schoon or "/api/" in schoon: + return schoon + m = re.match(r"^(https?)://([^/]+)/([^/]+)/([^/]+?)(?:/releases.*|\.git)?$", + schoon) + if not m: + return schoon + schema, host, eigenaar, repo = m.groups() + if host.lower() in ("github.com", "www.github.com"): + return f"https://api.github.com/repos/{eigenaar}/{repo}/releases" + return f"{schema}://{host}/api/v1/repos/{eigenaar}/{repo}/releases" + + def _releases(url: str) -> list[dict]: req = urllib.request.Request( url, headers={"Accept": "application/json", "User-Agent": "server-up"}) with urllib.request.urlopen(req, timeout=8) as r: - data = json.loads(r.read().decode("utf-8")) + rauw = r.read().decode("utf-8", "replace") + try: + data = json.loads(rauw) + except ValueError: + # Bijna altijd een webpagina in plaats van de API. Zeg dát, in plaats + # van de gebruiker met een JSON-fout te laten zitten. + raise ValueError( + f"{url} geeft geen JSON terug maar een webpagina. Vul het " + "API-adres in, met /api/v1/repos/ erin.") from None return data if isinstance(data, list) else [data] @@ -98,7 +133,7 @@ def check(current: str, force: bool = False) -> dict: return dict(_CACHE) c = cfg.load() - url = (c.get("UPDATE_API_URL") or "").strip() + url = api_url(c.get("UPDATE_API_URL") or "") ch = channel() res = {"enabled": bool(url), "current": current, "channel": ch, "current_is_prerelease": is_prerelease(current), diff --git a/server-up/translations/en.json b/server-up/translations/en.json index ac72adc..8bd1fe6 100644 --- a/server-up/translations/en.json +++ b/server-up/translations/en.json @@ -138,7 +138,7 @@ "release_notes": "Release notes", "view_release": "View release", "update_api_url": "Releases API URL", - "update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.", + "update_api_hint": "Address of your releases. You may paste the address from your browser; Server Up converts it to the API address with /api/v1/repos/ itself. Empty = disabled.", "include_prerelease": "Include pre-releases (beta)", "login": "Sign in", "login_failed": "Sign-in failed", diff --git a/server-up/translations/nl.json b/server-up/translations/nl.json index f013c24..589bcc5 100644 --- a/server-up/translations/nl.json +++ b/server-up/translations/nl.json @@ -138,7 +138,7 @@ "release_notes": "Release notes", "view_release": "Bekijk release", "update_api_url": "Releases-API URL", - "update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.", + "update_api_hint": "Adres van je releases. Je mag het adres uit de adresbalk plakken; Server Up rekent het zelf om naar het API-adres met /api/v1/repos/. Leeg = uitgeschakeld.", "include_prerelease": "Pre-releases (beta) meenemen", "login": "Inloggen", "login_failed": "Inloggen mislukt", diff --git a/tests/test_update_adres.py b/tests/test_update_adres.py new file mode 100644 index 0000000..66bcc52 --- /dev/null +++ b/tests/test_update_adres.py @@ -0,0 +1,119 @@ +"""Het adres van de releases-API en van het image, zoals mensen ze invullen. + +Wie in Forgejo naar zijn releases kijkt en dat adres uit de adresbalk plakt, +geeft het adres van een webpagina op. De enige melding die daarop volgde was +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`, en daar leidt +niemand uit af dat er `/api/v1/repos/` in had moeten staan. +""" +import sys +from pathlib import Path + +import pytest + +WORTEL = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(WORTEL / "server-up")) + + +# ── Het releases-adres ─────────────────────────────────────────────────────── + +@pytest.mark.parametrize("ingevuld,verwacht", [ + # Wat je uit de adresbalk van Forgejo plakt. + ("https://git.voorbeeld.nl/bes-r/server-up/releases", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # Zonder /releases erachter. + ("https://git.voorbeeld.nl/bes-r/server-up", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # Met poort, en http. + ("http://10.0.20.22:3000/bes-r/server-up/releases", + "http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases"), + # De kloon-URL. + ("https://git.voorbeeld.nl/bes-r/server-up.git", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # GitHub heeft zijn API op een eigen host. + ("https://github.com/bes-r/server-up/releases", + "https://api.github.com/repos/bes-r/server-up/releases"), +]) +def test_webadres_wordt_een_apiadres(env, ingevuld, verwacht): + from core import updater + assert updater.api_url(ingevuld) == verwacht + + +@pytest.mark.parametrize("al_goed", [ + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases", + "https://api.github.com/repos/bes-r/server-up/releases", +]) +def test_een_apiadres_blijft_ongemoeid(env, al_goed): + from core import updater + assert updater.api_url(al_goed) == al_goed + + +def test_leeg_blijft_leeg(env): + from core import updater + assert updater.api_url("") == "" + assert updater.api_url(" ") == "" + + +def test_onherkenbaar_adres_blijft_staan(env): + """Niet slimmer doen dan we zijn: wat we niet herkennen laten we met rust, + zodat een eigen proxy-adres blijft werken.""" + from core import updater + raar = "https://voorbeeld.nl/een/diep/pad/met/releases.json" + assert updater.api_url(raar) == raar + + +def test_html_geeft_een_begrijpelijke_fout(env, monkeypatch): + """De melding moet zeggen wát er mis is, niet dat JSON niet parseert.""" + from core import updater + + class _Antwoord: + def read(self): + return b"\n" + def __enter__(self): + return self + def __exit__(self, *a): + return False + + monkeypatch.setattr(updater.urllib.request, "urlopen", + lambda *a, **k: _Antwoord()) + with pytest.raises(ValueError) as fout: + updater._releases("https://git.voorbeeld.nl/bes-r/server-up/releases") + melding = str(fout.value) + assert "webpagina" in melding + assert "/api/v1/repos/" in melding + assert "JSONDecode" not in melding + + +def test_check_gebruikt_het_omgerekende_adres(env, monkeypatch): + from core import updater + env["core"].patch({"UPDATE_API_URL": + "https://git.voorbeeld.nl/bes-r/server-up/releases"}) + gebruikt = [] + monkeypatch.setattr(updater, "_releases", + lambda u: gebruikt.append(u) or [ + {"tag_name": "v9.9.99", "prerelease": False}]) + res = updater.check("0.8.28", force=True) + assert gebruikt == [ + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"] + assert res["update_available"] is True + + +# ── Het imageadres ─────────────────────────────────────────────────────────── + +def test_schema_voor_het_image_wordt_weggehaald(env): + """`docker pull https://…` bestaat niet, maar in het veld ziet het er goed + uit. Er is geen geldig image dat met een schema begint, dus weghalen kan.""" + from core import selfupdate + env["core"].patch({"UPDATE_IMAGE": "https://git.voorbeeld.nl/bes-r/server-up"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up" + + env["core"].patch({"UPDATE_IMAGE": "http://10.0.20.22:3000/bes-r/server-up/"}) + assert selfupdate.image_repo({}) == "10.0.20.22:3000/bes-r/server-up" + + +def test_image_zonder_schema_blijft_gelijk(env): + from core import selfupdate + env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up" + # Een tag hoort er niet in; die komt van de release. + env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up:0.8.28"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up"