diff --git a/scripts/jellyfin-providers.sh b/scripts/jellyfin-providers.sh index c982b65..9436e9d 100755 --- a/scripts/jellyfin-providers.sh +++ b/scripts/jellyfin-providers.sh @@ -135,7 +135,18 @@ text = escape(val) if pat.search(src): out = pat.sub(lambda m: f"{m.group(1)}<{elem}>{text}", src, count=1) else: - out = src.replace("", f" <{elem}>{text}\n", 1) + # The plugin owns this file and writes it under ITS OWN root element + # (TheTVDB's is ), and its serializer omits a + # null-valued setting entirely — so the element being filled can be + # absent outright, and a hardcoded anchor then matches + # nothing. Anchor on the document's real root instead. + out = src + root = re.search(r"<([A-Za-z_][\w.:-]*)[^>]*>", src) + if root and ("" % root.group(1)) in src: + name = root.group(1) + out = src.replace( + "" % name, f" <{elem}>{text}\n", 1 + ) if out == src: raise SystemExit(f"{xml_path}: no place to write <{elem}>") diff --git a/tests/jellyfin_providers.bats b/tests/jellyfin_providers.bats index c9e71d0..416da6e 100755 --- a/tests/jellyfin_providers.bats +++ b/tests/jellyfin_providers.bats @@ -13,9 +13,12 @@ # 3. a downloaded plugin artifact is accepted only on its pinned sha256, its # official guid and its pinned version (a swapped or downgraded artifact # is refused before anything lands in plugins/); -# 4. the TMDb key is written into the EXISTING config with every other -# setting preserved — Jellyfin owns that file, and clobbering it would -# silently reset country, language and adult settings; +# 4. a key is written into the EXISTING config with every other setting +# preserved, under whatever root that file's owner chose — Jellyfin's core +# configs say ``, TheTVDB writes `` and +# omits a null setting outright, so the element to fill can be absent and +# the anchor has to come from the file; clobbering the file would silently +# reset country, language and adult settings; # 5. a TVDB config the operator already saved through the UI is kept; # 6. install is idempotent: a second run changes no byte; # 7. whatever is replaced is copied to the trash first (fleet law: never @@ -254,6 +257,38 @@ XML grep -qF 'true' "$cfg" } +@test "the key lands in a plugin-written config with no ApiKey element" { + # What TheTVDB actually writes on a fresh install: its own root element and + # no at all, because its serializer drops null settings. The + # installer used to anchor on and refuse this file. + cat >"$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" < + + + 1 + true + +XML + run prov install + [ "$status" -eq 0 ] + printf '%s\n' "${lines[@]}" | grep -qiF 'filled the ApiKey' + local cfg="$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" + grep -qF "$TVDB_KEY" "$cfg" + grep -qF '1' "$cfg" + grep -qF 'true' "$cfg" +} + +@test "a config file with no root element is refused by name" { + # The guard still has to fire when there is genuinely nowhere to write — + # a refusal naming the file beats inventing a document shape. + printf '\n' \ + >"$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" + run prov install + [ "$status" -ne 0 ] + printf '%s\n' "${lines[@]}" | grep -qF 'Jellyfin.Plugin.Tvdb.xml' + printf '%s\n' "${lines[@]}" | grep -qiF 'no place to write' +} + @test "a pin in the TVDB secret lands in the plugin config" { # A subscriber-shaped key needs the account's PIN beside it; a licensed key # does not, so the element appears only when the secret carries one.