mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-10 11:48:56 +02:00
722 lines
20 KiB
Python
722 lines
20 KiB
Python
"""Every client method sends exactly the request the app documents.
|
|
|
|
The table below is the whole public surface. Each row names a method, the
|
|
arguments to call it with, and the request that must appear on the wire: the
|
|
verb, the concrete path, the query string and the JSON body. ``operation`` is
|
|
the path template the app publishes, which ties this file to
|
|
``donutbrowser.coverage.OPERATIONS`` and, through it, to ``sdk/api-paths.json``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import pytest
|
|
from fake_donut import FakeDonut
|
|
|
|
from donutbrowser import DonutClient
|
|
from donutbrowser.coverage import OPERATIONS
|
|
|
|
Case = Tuple[
|
|
str, # client method
|
|
Tuple[Any, ...], # positional arguments
|
|
Dict[str, Any], # keyword arguments
|
|
str, # expected verb
|
|
str, # expected concrete path
|
|
Optional[Dict[str, Any]], # expected JSON body, or None for no body
|
|
Dict[str, str], # expected query string
|
|
str, # operation template, as published by the app
|
|
]
|
|
|
|
LOCATOR = {"role": "button", "name": "Sign in"}
|
|
|
|
CASES: List[Case] = [
|
|
# -- profiles ----------------------------------------------------------
|
|
("list_profiles", (), {}, "GET", "/v1/profiles", None, {}, "/v1/profiles"),
|
|
("get_profile", ("p1",), {}, "GET", "/v1/profiles/p1", None, {}, "/v1/profiles/{id}"),
|
|
(
|
|
"create_profile",
|
|
(),
|
|
{"name": "Shopper", "browser": "wayfern", "tags": ["eu"], "ephemeral": True},
|
|
"POST",
|
|
"/v1/profiles",
|
|
{"name": "Shopper", "browser": "wayfern", "tags": ["eu"], "ephemeral": True},
|
|
{},
|
|
"/v1/profiles",
|
|
),
|
|
(
|
|
"create_profile",
|
|
(),
|
|
{"name": "Bare", "browser": "wayfern"},
|
|
"POST",
|
|
"/v1/profiles",
|
|
{"name": "Bare", "browser": "wayfern"},
|
|
{},
|
|
"/v1/profiles",
|
|
),
|
|
(
|
|
"update_profile",
|
|
("p1",),
|
|
{"name": "Renamed", "proxy_id": "", "clear_on_close": False},
|
|
"PUT",
|
|
"/v1/profiles/p1",
|
|
{"name": "Renamed", "proxy_id": "", "clear_on_close": False},
|
|
{},
|
|
"/v1/profiles/{id}",
|
|
),
|
|
("delete_profile", ("p1",), {}, "DELETE", "/v1/profiles/p1", None, {}, "/v1/profiles/{id}"),
|
|
(
|
|
"run_profile",
|
|
("p1",),
|
|
{"url": "https://example.com", "headless": True},
|
|
"POST",
|
|
"/v1/profiles/p1/run",
|
|
{"url": "https://example.com", "headless": True},
|
|
{},
|
|
"/v1/profiles/{id}/run",
|
|
),
|
|
(
|
|
"run_profile_remote",
|
|
("p1",),
|
|
{"url": "https://example.com"},
|
|
"POST",
|
|
"/v1/profiles/p1/run-remote",
|
|
{"url": "https://example.com"},
|
|
{},
|
|
"/v1/profiles/{id}/run-remote",
|
|
),
|
|
(
|
|
"set_profile_cloud_sync",
|
|
("p1",),
|
|
{"mode": "Regular"},
|
|
"POST",
|
|
"/v1/profiles/p1/cloud-sync",
|
|
{"mode": "Regular"},
|
|
{},
|
|
"/v1/profiles/{id}/cloud-sync",
|
|
),
|
|
(
|
|
"open_url",
|
|
("p1", "https://example.com/page"),
|
|
{},
|
|
"POST",
|
|
"/v1/profiles/p1/open-url",
|
|
{"url": "https://example.com/page"},
|
|
{},
|
|
"/v1/profiles/{id}/open-url",
|
|
),
|
|
(
|
|
"kill_profile",
|
|
("p1",),
|
|
{},
|
|
"POST",
|
|
"/v1/profiles/p1/kill",
|
|
None,
|
|
{},
|
|
"/v1/profiles/{id}/kill",
|
|
),
|
|
(
|
|
"batch_run_profiles",
|
|
(["p1", "p2"],),
|
|
{"headless": False},
|
|
"POST",
|
|
"/v1/profiles/batch/run",
|
|
{"profile_ids": ["p1", "p2"], "headless": False},
|
|
{},
|
|
"/v1/profiles/batch/run",
|
|
),
|
|
(
|
|
"batch_stop_profiles",
|
|
(["p1", "p2"],),
|
|
{},
|
|
"POST",
|
|
"/v1/profiles/batch/stop",
|
|
{"profile_ids": ["p1", "p2"]},
|
|
{},
|
|
"/v1/profiles/batch/stop",
|
|
),
|
|
(
|
|
"distribute_proxies",
|
|
([{"profile_id": "p1", "proxy_id": "x1"}, {"profile_id": "p2", "proxy_id": "x2"}],),
|
|
{},
|
|
"POST",
|
|
"/v1/profiles/distribute-proxies",
|
|
{
|
|
"pairs": [
|
|
{"profile_id": "p1", "proxy_id": "x1"},
|
|
{"profile_id": "p2", "proxy_id": "x2"},
|
|
]
|
|
},
|
|
{},
|
|
"/v1/profiles/distribute-proxies",
|
|
),
|
|
(
|
|
"detect_import_profiles",
|
|
(),
|
|
{"folder": "/Users/x/Chrome"},
|
|
"GET",
|
|
"/v1/profiles/import/detect",
|
|
None,
|
|
{"folder": "/Users/x/Chrome"},
|
|
"/v1/profiles/import/detect",
|
|
),
|
|
(
|
|
"detect_import_profiles",
|
|
(),
|
|
{},
|
|
"GET",
|
|
"/v1/profiles/import/detect",
|
|
None,
|
|
{},
|
|
"/v1/profiles/import/detect",
|
|
),
|
|
(
|
|
"import_profiles",
|
|
([{"source_path": "/tmp/src", "new_profile_name": "Imported"}],),
|
|
{"duplicate_strategy": "skip"},
|
|
"POST",
|
|
"/v1/profiles/import",
|
|
{
|
|
"items": [{"source_path": "/tmp/src", "new_profile_name": "Imported"}],
|
|
"duplicate_strategy": "skip",
|
|
},
|
|
{},
|
|
"/v1/profiles/import",
|
|
),
|
|
(
|
|
"import_profile_cookies",
|
|
("p1",),
|
|
{"content": "[]"},
|
|
"POST",
|
|
"/v1/profiles/p1/cookies/import",
|
|
{"content": "[]"},
|
|
{},
|
|
"/v1/profiles/{id}/cookies/import",
|
|
),
|
|
# -- agent -------------------------------------------------------------
|
|
(
|
|
"agent_perceive",
|
|
("p1",),
|
|
{"viewport_only": True, "max_bytes": 2048},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/perceive",
|
|
{"max_bytes": 2048, "viewport_only": True},
|
|
{},
|
|
"/v1/profiles/{id}/agent/perceive",
|
|
),
|
|
(
|
|
"agent_perceive",
|
|
("p1",),
|
|
{},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/perceive",
|
|
{},
|
|
{},
|
|
"/v1/profiles/{id}/agent/perceive",
|
|
),
|
|
(
|
|
"agent_resolve_locator",
|
|
("p1",),
|
|
{"locator": LOCATOR, "candidate_limit": 5},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/resolve-locator",
|
|
{"locator": LOCATOR, "candidate_limit": 5},
|
|
{},
|
|
"/v1/profiles/{id}/agent/resolve-locator",
|
|
),
|
|
(
|
|
"agent_click",
|
|
("p1",),
|
|
{"locator": LOCATOR, "button": "right", "click_count": 2},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/click",
|
|
{"locator": LOCATOR, "button": "right", "click_count": 2},
|
|
{},
|
|
"/v1/profiles/{id}/agent/click",
|
|
),
|
|
(
|
|
"agent_type",
|
|
("p1",),
|
|
{"locator": LOCATOR, "text": "hello", "clear_first": False, "wpm": 55.0},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/type",
|
|
{"locator": LOCATOR, "text": "hello", "clear_first": False, "wpm": 55.0},
|
|
{},
|
|
"/v1/profiles/{id}/agent/type",
|
|
),
|
|
(
|
|
"agent_extract",
|
|
("p1",),
|
|
{
|
|
"container": {"role": "listitem"},
|
|
"field_map": [{"key": "title", "locator": {"role": "heading"}, "source": "text"}],
|
|
"max_pages": 3,
|
|
},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/extract",
|
|
{
|
|
"container": {"role": "listitem"},
|
|
"field_map": [{"key": "title", "locator": {"role": "heading"}, "source": "text"}],
|
|
"max_pages": 3,
|
|
},
|
|
{},
|
|
"/v1/profiles/{id}/agent/extract",
|
|
),
|
|
(
|
|
"agent_pick",
|
|
("p1",),
|
|
{"timeout_ms": 15000},
|
|
"POST",
|
|
"/v1/profiles/p1/agent/pick",
|
|
{"timeout_ms": 15000},
|
|
{},
|
|
"/v1/profiles/{id}/agent/pick",
|
|
),
|
|
# -- remote sessions ---------------------------------------------------
|
|
(
|
|
"list_remote_sessions",
|
|
(),
|
|
{},
|
|
"GET",
|
|
"/v1/remote-sessions",
|
|
None,
|
|
{},
|
|
"/v1/remote-sessions",
|
|
),
|
|
(
|
|
"get_remote_session",
|
|
("s1",),
|
|
{},
|
|
"GET",
|
|
"/v1/remote-sessions/s1",
|
|
None,
|
|
{},
|
|
"/v1/remote-sessions/{id}",
|
|
),
|
|
(
|
|
"stop_remote_session",
|
|
("s1",),
|
|
{},
|
|
"DELETE",
|
|
"/v1/remote-sessions/s1",
|
|
None,
|
|
{},
|
|
"/v1/remote-sessions/{id}",
|
|
),
|
|
("get_remote_hours", (), {}, "GET", "/v1/remote-hours", None, {}, "/v1/remote-hours"),
|
|
# -- cookie bot --------------------------------------------------------
|
|
(
|
|
"list_cookie_bot_schedules",
|
|
(),
|
|
{"scope": "team"},
|
|
"GET",
|
|
"/v1/cookie-bot/schedules",
|
|
None,
|
|
{"scope": "team"},
|
|
"/v1/cookie-bot/schedules",
|
|
),
|
|
(
|
|
"get_cookie_bot_schedule",
|
|
("p1",),
|
|
{},
|
|
"GET",
|
|
"/v1/cookie-bot/schedules/p1",
|
|
None,
|
|
{},
|
|
"/v1/cookie-bot/schedules/{profile_id}",
|
|
),
|
|
(
|
|
"set_cookie_bot_schedule",
|
|
("p1",),
|
|
{
|
|
"enabled": True,
|
|
"run_at_minute": 120,
|
|
"days_mask": 31,
|
|
"timezone": "Europe/Berlin",
|
|
"preset": "steady",
|
|
"max_minutes": 45,
|
|
"sites": ["https://example.com"],
|
|
"acknowledge_conflict": True,
|
|
},
|
|
"PUT",
|
|
"/v1/cookie-bot/schedules/p1",
|
|
{
|
|
"enabled": True,
|
|
"run_at_minute": 120,
|
|
"days_mask": 31,
|
|
"timezone": "Europe/Berlin",
|
|
"preset": "steady",
|
|
"max_minutes": 45,
|
|
"sites": ["https://example.com"],
|
|
"acknowledge_conflict": True,
|
|
},
|
|
{},
|
|
"/v1/cookie-bot/schedules/{profile_id}",
|
|
),
|
|
(
|
|
"delete_cookie_bot_schedule",
|
|
("p1",),
|
|
{},
|
|
"DELETE",
|
|
"/v1/cookie-bot/schedules/p1",
|
|
None,
|
|
{},
|
|
"/v1/cookie-bot/schedules/{profile_id}",
|
|
),
|
|
(
|
|
"get_cookie_bot_conflicts",
|
|
("p1",),
|
|
{"run_at_minute": 90, "timezone": "UTC", "days_mask": 7},
|
|
"GET",
|
|
"/v1/cookie-bot/conflicts",
|
|
None,
|
|
{"profile_id": "p1", "run_at_minute": "90", "timezone": "UTC", "days_mask": "7"},
|
|
"/v1/cookie-bot/conflicts",
|
|
),
|
|
(
|
|
"list_cookie_bot_runs",
|
|
(),
|
|
{"profile_id": "p1", "limit": 10, "before": "cursor-1"},
|
|
"GET",
|
|
"/v1/cookie-bot/runs",
|
|
None,
|
|
{"profile_id": "p1", "limit": "10", "before": "cursor-1"},
|
|
"/v1/cookie-bot/runs",
|
|
),
|
|
(
|
|
"start_cookie_bot_run",
|
|
(),
|
|
{"profile_id": "p1", "max_minutes": 30},
|
|
"POST",
|
|
"/v1/cookie-bot/runs",
|
|
{"profile_id": "p1", "max_minutes": 30},
|
|
{},
|
|
"/v1/cookie-bot/runs",
|
|
),
|
|
(
|
|
"cancel_cookie_bot_run",
|
|
("r1",),
|
|
{},
|
|
"DELETE",
|
|
"/v1/cookie-bot/runs/r1",
|
|
None,
|
|
{},
|
|
"/v1/cookie-bot/runs/{run_id}",
|
|
),
|
|
(
|
|
"list_cookie_bot_presets",
|
|
(),
|
|
{},
|
|
"GET",
|
|
"/v1/cookie-bot/presets",
|
|
None,
|
|
{},
|
|
"/v1/cookie-bot/presets",
|
|
),
|
|
(
|
|
"get_cookie_bot_usage",
|
|
(),
|
|
{"period": "2026-08"},
|
|
"GET",
|
|
"/v1/cookie-bot/usage",
|
|
None,
|
|
{"period": "2026-08"},
|
|
"/v1/cookie-bot/usage",
|
|
),
|
|
# -- groups and tags ---------------------------------------------------
|
|
("list_groups", (), {}, "GET", "/v1/groups", None, {}, "/v1/groups"),
|
|
("get_group", ("g1",), {}, "GET", "/v1/groups/g1", None, {}, "/v1/groups/{id}"),
|
|
("create_group", (), {"name": "Retail"}, "POST", "/v1/groups", {"name": "Retail"}, {}, "/v1/groups"),
|
|
(
|
|
"update_group",
|
|
("g1",),
|
|
{"name": "Retail EU"},
|
|
"PUT",
|
|
"/v1/groups/g1",
|
|
{"name": "Retail EU"},
|
|
{},
|
|
"/v1/groups/{id}",
|
|
),
|
|
("delete_group", ("g1",), {}, "DELETE", "/v1/groups/g1", None, {}, "/v1/groups/{id}"),
|
|
("list_tags", (), {}, "GET", "/v1/tags", None, {}, "/v1/tags"),
|
|
# -- proxies -----------------------------------------------------------
|
|
("list_proxies", (), {}, "GET", "/v1/proxies", None, {}, "/v1/proxies"),
|
|
("get_proxy", ("x1",), {}, "GET", "/v1/proxies/x1", None, {}, "/v1/proxies/{id}"),
|
|
(
|
|
"create_proxy",
|
|
(),
|
|
{"name": "EU", "proxy_settings": {"proxy_type": "http", "host": "h", "port": 8080}},
|
|
"POST",
|
|
"/v1/proxies",
|
|
{"name": "EU", "proxy_settings": {"proxy_type": "http", "host": "h", "port": 8080}},
|
|
{},
|
|
"/v1/proxies",
|
|
),
|
|
(
|
|
"update_proxy",
|
|
("x1",),
|
|
{"name": "EU 2"},
|
|
"PUT",
|
|
"/v1/proxies/x1",
|
|
{"name": "EU 2"},
|
|
{},
|
|
"/v1/proxies/{id}",
|
|
),
|
|
("delete_proxy", ("x1",), {}, "DELETE", "/v1/proxies/x1", None, {}, "/v1/proxies/{id}"),
|
|
(
|
|
"import_proxies",
|
|
(),
|
|
{"format": "txt", "content": "h:1:u:p", "name_prefix": "EU"},
|
|
"POST",
|
|
"/v1/proxies/import",
|
|
{"format": "txt", "content": "h:1:u:p", "name_prefix": "EU"},
|
|
{},
|
|
"/v1/proxies/import",
|
|
),
|
|
# -- vpns --------------------------------------------------------------
|
|
("list_vpns", (), {}, "GET", "/v1/vpns", None, {}, "/v1/vpns"),
|
|
("get_vpn", ("v1",), {}, "GET", "/v1/vpns/v1", None, {}, "/v1/vpns/{id}"),
|
|
("export_vpn", ("v1",), {}, "GET", "/v1/vpns/v1/export", None, {}, "/v1/vpns/{id}/export"),
|
|
(
|
|
"import_vpn",
|
|
(),
|
|
{"content": "[Interface]", "filename": "eu.conf"},
|
|
"POST",
|
|
"/v1/vpns/import",
|
|
{"content": "[Interface]", "filename": "eu.conf"},
|
|
{},
|
|
"/v1/vpns/import",
|
|
),
|
|
(
|
|
"create_vpn",
|
|
(),
|
|
{"name": "EU", "vpn_type": "WireGuard", "config_data": "[Interface]"},
|
|
"POST",
|
|
"/v1/vpns",
|
|
{"name": "EU", "vpn_type": "WireGuard", "config_data": "[Interface]"},
|
|
{},
|
|
"/v1/vpns",
|
|
),
|
|
(
|
|
"update_vpn",
|
|
("v1",),
|
|
{"name": "EU 2"},
|
|
"PUT",
|
|
"/v1/vpns/v1",
|
|
{"name": "EU 2"},
|
|
{},
|
|
"/v1/vpns/{id}",
|
|
),
|
|
("delete_vpn", ("v1",), {}, "DELETE", "/v1/vpns/v1", None, {}, "/v1/vpns/{id}"),
|
|
# -- extensions --------------------------------------------------------
|
|
("list_extensions", (), {}, "GET", "/v1/extensions", None, {}, "/v1/extensions"),
|
|
("get_extension", ("e1",), {}, "GET", "/v1/extensions/e1", None, {}, "/v1/extensions/{id}"),
|
|
(
|
|
"create_extension",
|
|
(),
|
|
{"name": "Blocker", "file_name": "b.crx", "file_data_base64": "AAAA"},
|
|
"POST",
|
|
"/v1/extensions",
|
|
{"name": "Blocker", "file_name": "b.crx", "file_data_base64": "AAAA"},
|
|
{},
|
|
"/v1/extensions",
|
|
),
|
|
(
|
|
"update_extension",
|
|
("e1",),
|
|
{"name": "Blocker 2", "link": True},
|
|
"PUT",
|
|
"/v1/extensions/e1",
|
|
{"name": "Blocker 2", "link": True},
|
|
{},
|
|
"/v1/extensions/{id}",
|
|
),
|
|
(
|
|
"delete_extension",
|
|
("e1",),
|
|
{},
|
|
"DELETE",
|
|
"/v1/extensions/e1",
|
|
None,
|
|
{},
|
|
"/v1/extensions/{id}",
|
|
),
|
|
(
|
|
"list_extension_groups",
|
|
(),
|
|
{},
|
|
"GET",
|
|
"/v1/extension-groups",
|
|
None,
|
|
{},
|
|
"/v1/extension-groups",
|
|
),
|
|
(
|
|
"get_extension_group",
|
|
("eg1",),
|
|
{},
|
|
"GET",
|
|
"/v1/extension-groups/eg1",
|
|
None,
|
|
{},
|
|
"/v1/extension-groups/{id}",
|
|
),
|
|
(
|
|
"create_extension_group",
|
|
(),
|
|
{"name": "Adblock set"},
|
|
"POST",
|
|
"/v1/extension-groups",
|
|
{"name": "Adblock set"},
|
|
{},
|
|
"/v1/extension-groups",
|
|
),
|
|
(
|
|
"update_extension_group",
|
|
("eg1",),
|
|
{"extension_ids": ["e1", "e2"]},
|
|
"PUT",
|
|
"/v1/extension-groups/eg1",
|
|
{"extension_ids": ["e1", "e2"]},
|
|
{},
|
|
"/v1/extension-groups/{id}",
|
|
),
|
|
(
|
|
"delete_extension_group",
|
|
("eg1",),
|
|
{},
|
|
"DELETE",
|
|
"/v1/extension-groups/eg1",
|
|
None,
|
|
{},
|
|
"/v1/extension-groups/{id}",
|
|
),
|
|
(
|
|
"add_extension_to_group",
|
|
("eg1", "e1"),
|
|
{},
|
|
"POST",
|
|
"/v1/extension-groups/eg1/extensions/e1",
|
|
None,
|
|
{},
|
|
"/v1/extension-groups/{id}/extensions/{extension_id}",
|
|
),
|
|
(
|
|
"remove_extension_from_group",
|
|
("eg1", "e1"),
|
|
{},
|
|
"DELETE",
|
|
"/v1/extension-groups/eg1/extensions/e1",
|
|
None,
|
|
{},
|
|
"/v1/extension-groups/{id}/extensions/{extension_id}",
|
|
),
|
|
# -- browsers ----------------------------------------------------------
|
|
(
|
|
"download_browser",
|
|
(),
|
|
{"browser": "wayfern", "version": "152.0.1"},
|
|
"POST",
|
|
"/v1/browsers/download",
|
|
{"browser": "wayfern", "version": "152.0.1"},
|
|
{},
|
|
"/v1/browsers/download",
|
|
),
|
|
(
|
|
"list_browser_versions",
|
|
("wayfern",),
|
|
{},
|
|
"GET",
|
|
"/v1/browsers/wayfern/versions",
|
|
None,
|
|
{},
|
|
"/v1/browsers/{browser}/versions",
|
|
),
|
|
(
|
|
"is_browser_downloaded",
|
|
("wayfern", "152.0.1"),
|
|
{},
|
|
"GET",
|
|
"/v1/browsers/wayfern/versions/152.0.1/downloaded",
|
|
None,
|
|
{},
|
|
"/v1/browsers/{browser}/versions/{version}/downloaded",
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"case", CASES, ids=[f"{case[0]}[{index}]" for index, case in enumerate(CASES)]
|
|
)
|
|
def test_method_sends_the_documented_request(
|
|
client: DonutClient, fake: FakeDonut, case: Case
|
|
) -> None:
|
|
name, args, kwargs, verb, path, body, query, operation = case
|
|
|
|
getattr(client, name)(*args, **kwargs)
|
|
|
|
sent = fake.last
|
|
assert sent.method == verb
|
|
assert sent.path == path
|
|
assert sent.query == query
|
|
assert sent.json == body
|
|
assert OPERATIONS[(verb, operation)] == name
|
|
|
|
|
|
def test_every_client_method_is_exercised_here() -> None:
|
|
"""No method may be added to the table of operations without a case above."""
|
|
covered = {case[0] for case in CASES}
|
|
missing = sorted(set(OPERATIONS.values()) - covered)
|
|
assert not missing, f"these wrapped operations have no request test: {missing}"
|
|
|
|
|
|
def test_the_token_travels_as_a_bearer_header(client: DonutClient, fake: FakeDonut) -> None:
|
|
client.list_profiles()
|
|
sent = fake.last
|
|
assert sent.header("Authorization") == "Bearer test-token-abc123"
|
|
assert sent.header("Accept") == "application/json"
|
|
assert sent.header("Content-Type") is None, "a GET must not claim to carry JSON"
|
|
|
|
|
|
def test_a_body_is_sent_as_json(client: DonutClient, fake: FakeDonut) -> None:
|
|
client.create_group(name="Retail")
|
|
sent = fake.last
|
|
assert sent.header("Content-Type") == "application/json"
|
|
assert json.loads(sent.body.decode()) == {"name": "Retail"}
|
|
|
|
|
|
def test_path_ids_are_escaped(client: DonutClient, fake: FakeDonut) -> None:
|
|
"""An id can never break out of its own path segment."""
|
|
client.get_profile("a/b c?d")
|
|
assert fake.last.path == "/v1/profiles/a%2Fb%20c%3Fd"
|
|
|
|
|
|
def test_none_arguments_are_left_out_of_the_body(client: DonutClient, fake: FakeDonut) -> None:
|
|
client.update_profile("p1", name="Only this")
|
|
assert fake.last.json == {"name": "Only this"}
|
|
|
|
|
|
def test_an_empty_string_still_reaches_the_app(client: DonutClient, fake: FakeDonut) -> None:
|
|
"""`proxy_id=""` is how the app is told to detach a proxy, so it must survive."""
|
|
client.update_profile("p1", proxy_id="")
|
|
assert fake.last.json == {"proxy_id": ""}
|
|
|
|
|
|
def test_a_no_content_answer_becomes_none(client: DonutClient, fake: FakeDonut) -> None:
|
|
fake.enqueue_empty(204)
|
|
assert client.delete_profile("p1") is None
|
|
|
|
|
|
def test_a_json_answer_is_returned_as_sent(client: DonutClient, fake: FakeDonut) -> None:
|
|
fake.enqueue_json({"profiles": [{"id": "p1", "name": "Shopper"}], "total": 1})
|
|
assert client.list_profiles() == {
|
|
"profiles": [{"id": "p1", "name": "Shopper"}],
|
|
"total": 1,
|
|
}
|
|
|
|
|
|
def test_a_bare_boolean_answer_is_returned(client: DonutClient, fake: FakeDonut) -> None:
|
|
fake.enqueue_json(True)
|
|
assert client.is_browser_downloaded("wayfern", "152.0.1") is True
|