mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
fix(remote): resolve service names against the remote server
Remote mode resolved names with the local alias table and built an empty stub for any unknown tag, so an unentitled service showed a blank help page. Resolve against the server's tags and aliases, and reject unknown names with the list the server does offer.
This commit is contained in:
@@ -101,7 +101,9 @@ In `--remote` mode unshackle turns the server's service list into synthetic CLI
|
||||
commands that operate against it, falling back to the tags in that `services` sub-dict when
|
||||
unshackle cannot fetch the list. Each synthetic command carries the server-side service's options and
|
||||
documentation, so `unshackle dl --remote <TAG> -h` shows the same help text as it does on the
|
||||
server. See [remote sessions](../../dev/rest-api/remote-sessions.md) for the
|
||||
server. Service names resolve against the server's tags and aliases, not your local services.
|
||||
If the server does not offer a name to your API key, unshackle rejects the name and lists the
|
||||
tags it does offer. See [remote sessions](../../dev/rest-api/remote-sessions.md) for the
|
||||
full setup.
|
||||
|
||||
## `serve`
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""make_remote_command resolves names against the remote server's list and reports the failures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
|
||||
from unshackle.core.services import Services
|
||||
|
||||
SERVED = [
|
||||
{"tag": "AMZN", "url": "https://example.com", "help": "Amazon.", "cli_params": [], "aliases": []},
|
||||
{"tag": "HMAX", "url": "https://example.com", "help": "Max.", "cli_params": [], "aliases": ["MAX", "hbomax"]},
|
||||
]
|
||||
|
||||
|
||||
def _make(name: str) -> click.Command:
|
||||
return Services.make_remote_command(name, ctx=click.Context(click.Command("dl")))
|
||||
|
||||
|
||||
def test_unknown_name_raises_with_typed_name_and_available_list() -> None:
|
||||
with patch.object(Services, "fetch_remote_services", return_value=SERVED):
|
||||
with pytest.raises(click.ClickException, match=r"does not offer a service named 'BBC'.*AMZN, HMAX"):
|
||||
_make("BBC")
|
||||
|
||||
|
||||
def test_served_tag_builds_full_command() -> None:
|
||||
with patch.object(Services, "fetch_remote_services", return_value=SERVED):
|
||||
cmd = _make("AMZN")
|
||||
assert cmd.help == "Amazon."
|
||||
|
||||
|
||||
def test_server_alias_resolves_to_server_tag() -> None:
|
||||
with patch.object(Services, "fetch_remote_services", return_value=SERVED):
|
||||
cmd = _make("max")
|
||||
assert cmd.name == "HMAX"
|
||||
assert cmd.help == "Max."
|
||||
|
||||
|
||||
def test_empty_service_list_raises_no_services_message() -> None:
|
||||
with patch.object(Services, "fetch_remote_services", return_value=[]):
|
||||
with pytest.raises(click.ClickException, match=r"offers no services to your API key"):
|
||||
_make("AMZN")
|
||||
|
||||
|
||||
def test_fetch_failure_keeps_stub_instead_of_raising() -> None:
|
||||
with patch.object(Services, "fetch_remote_services", return_value=None):
|
||||
cmd = _make("AMZN")
|
||||
assert cmd.name == "AMZN"
|
||||
assert cmd.help is None
|
||||
+28
-17
@@ -300,7 +300,7 @@ class Services(click.Group):
|
||||
|
||||
remote = ctx.params.get("remote") or (ctx.parent and ctx.parent.params.get("remote"))
|
||||
if remote:
|
||||
return Services.make_remote_command(tag, ctx)
|
||||
return Services.make_remote_command(name, ctx)
|
||||
|
||||
try:
|
||||
service = Services.load(tag)
|
||||
@@ -355,9 +355,33 @@ class Services(click.Group):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def make_remote_command(tag: str, ctx: click.Context) -> click.Command:
|
||||
"""Make a Click command for a remote service with server-provided options."""
|
||||
svc_info = Services.fetch_remote_service_info(tag, ctx)
|
||||
def make_remote_command(name: str, ctx: click.Context) -> click.Command:
|
||||
"""Make a Click command for a remote service, with the options the remote server gives."""
|
||||
# A failed fetch (None) means the remote server was unreachable, not that the name is
|
||||
# invalid - fall through to a bare stub so `-h` keeps working offline.
|
||||
services = Services.fetch_remote_services(ctx)
|
||||
if services is None:
|
||||
svc_info = None
|
||||
tag = Services.get_tag(name)
|
||||
else:
|
||||
# Resolve against the remote server's tags and aliases, not the local tables: the
|
||||
# two drift apart as soon as either side's services change. Tags win over aliases,
|
||||
# as in local get_tag.
|
||||
lowered = name.lower()
|
||||
svc_info = next((svc for svc in services if str(svc.get("tag", "")).lower() == lowered), None)
|
||||
if svc_info is None:
|
||||
svc_info = next(
|
||||
(svc for svc in services if lowered in (str(a).lower() for a in svc.get("aliases") or [])), None
|
||||
)
|
||||
if svc_info is None:
|
||||
if services:
|
||||
available = ", ".join(sorted(svc["tag"] for svc in services))
|
||||
raise click.ClickException(
|
||||
f"The remote server does not offer a service named '{name}'. "
|
||||
f"It offers these services: {available}"
|
||||
)
|
||||
raise click.ClickException("The remote server offers no services to your API key.")
|
||||
tag = svc_info["tag"]
|
||||
short_help = svc_info.get("url") if svc_info else None
|
||||
help_text = svc_info.get("help") if svc_info else None
|
||||
if help_text:
|
||||
@@ -410,19 +434,6 @@ class Services(click.Group):
|
||||
|
||||
return import_cli
|
||||
|
||||
@staticmethod
|
||||
def fetch_remote_service_info(tag: str, ctx: click.Context) -> dict | None:
|
||||
"""Fetch service info for a specific service from the remote server."""
|
||||
try:
|
||||
services = Services.fetch_remote_services(ctx)
|
||||
if services:
|
||||
for svc in services:
|
||||
if svc.get("tag") == tag:
|
||||
return svc
|
||||
except Exception as e:
|
||||
log.debug(f"Remote service info lookup failed for {tag}: {e!r}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_tags() -> list[str]:
|
||||
"""Returns a list of service tags from all available Services."""
|
||||
|
||||
Reference in New Issue
Block a user