Files
creator-hub/cmd/docker_gateway/test_gateway.py
T

2524 lines
96 KiB
Python

from __future__ import annotations
import base64
import io
import json
import socket
import threading
import unittest
from collections import deque
from contextlib import contextmanager
from importlib import import_module
from typing import Any, cast
from unittest.mock import Mock, patch
import websocket
from .docker_client import (
BINDING_VERSION_LABEL,
BROWSER_NETWORK_ROLE,
GATEWAY_MEMBER_LABEL,
MANAGED_LABEL,
NETWORK_EXIT_LABEL,
NETWORK_ID_LABEL,
NETWORK_ROLE_LABEL,
RESERVATION_LABEL,
RESERVATION_OWNER_LABEL,
RUNTIME_ID_LABEL,
AliasReservationManager,
DockerClient,
DockerError,
DockerResponse,
GenerationConflict,
NetworkSetupError,
TenantNetworkGeneration,
UnmanagedContainer,
split_image_ref,
tenant_network_name,
)
from .douyin import (
BrowserResponse,
CDPConnection,
DouyinBrowser,
DouyinError,
DouyinSubscription,
SubscriptionManager,
ack_expression,
action_expression,
detect_challenge,
im_expression,
install_expression,
is_douyin_url,
normalize_notice,
notice_ids,
wait_expression,
)
from .proxy import (
MemoryProxy,
ProxyExit,
ProxyRegistry,
_copy_until_close,
_dial_http_proxy,
_dial_socks4,
_dial_socks5,
_is_ipv4,
_parse_request,
_read_request,
_read_status,
_recv_exact,
)
gateway_module = import_module(f"{__package__}.gateway")
douyin_module = import_module(f"{__package__}.douyin")
proxy_module = import_module(f"{__package__}.proxy")
docker_client_module = import_module(f"{__package__}.docker_client")
Gateway = gateway_module.Gateway
RequestError = gateway_module.RequestError
browser_tmpfs = gateway_module.browser_tmpfs
decode_generation = gateway_module.decode_generation
json_bytes = gateway_module.json_bytes
load_config = gateway_module.load_config
split_listen_address = gateway_module.split_listen_address
valid_douyin_url = gateway_module.valid_douyin_url
validate_create = gateway_module.validate_create
parse_proxy_exit = gateway_module.parse_proxy_exit
validate_proxy_exit = gateway_module.validate_proxy_exit
validate_proxy_restore = gateway_module.validate_proxy_restore
valid_douyin_generation = gateway_module.valid_douyin_generation
valid_account_key_query = gateway_module.valid_account_key_query
numeric_cursor = gateway_module.numeric_cursor
proxy_port = gateway_module.proxy_port
has_control = gateway_module.has_control
class FakeSocket:
def __init__(self, messages: list[object]) -> None:
self.messages = list(messages)
self.sent: list[bytes] = []
self.timeout = 0.0
def send(self, data: bytes) -> None:
self.sent.append(data)
def recv(self) -> str:
if not self.messages:
raise TimeoutError("no more messages")
return json.dumps(self.messages.pop(0))
def settimeout(self, value: float) -> None:
self.timeout = value
def close(self) -> None:
return None
class FakeConnection:
def __init__(self, values: list[object]) -> None:
self.values = list(values)
def evaluate(self, expression: str) -> object:
del expression
if not self.values:
raise DouyinError("no fake response")
return self.values.pop(0)
class FakeDocker:
def __init__(self, containers: list[dict] | None = None) -> None:
self.containers = containers or []
def container_network_address(self, container_id: str, network_id: str) -> str:
del container_id, network_id
return "192.0.2.10"
def request(
self, method: str, path: str, body: object | None = None
) -> DockerResponse:
del body
if method == "GET" and path.startswith("/containers/json"):
return DockerResponse(200, "OK", json.dumps(self.containers).encode())
return DockerResponse(404, "Not Found", b"")
class GatewayValidationTests(unittest.TestCase):
def test_create_and_generation_validation(self) -> None:
value = {
"alias": "safe-account",
"name": "Safe account",
"image": "creatorhub/browser:latest",
"cmd": ["about:blank"],
"volume": "creatorhub-safe-account",
"binding_version": 1,
"stopped": True,
}
validate_create(value)
self.assertEqual(value["network_exit"], ProxyExit("", "", 0))
self.assertEqual(value["network_exit_id"], "")
self.assertEqual(
decode_generation(
{
"binding_version": 1,
"runtime_id": "runtime-not-found",
"network_id": "network-id",
},
False,
False,
)["runtime_id"],
"runtime-not-found",
)
with self.assertRaises(RequestError):
validate_create(
{**value, "cmd": ["--proxy-server=http://x", "about:blank"]}
)
with self.assertRaises(RequestError):
decode_generation(
{"binding_version": 1, "runtime_id": 4, "network_id": "n"}, True, True
)
def test_config_and_urls(self) -> None:
self.assertEqual(split_listen_address(":8081"), ("", 8081))
self.assertEqual(split_listen_address("[::1]:8081"), ("::1", 8081))
with self.assertRaises(ValueError):
split_listen_address("missing-port")
config = load_config(
{
"LISTEN_ADDR": ":8081",
"DOCKER_SOCKET": "/var/run/docker.sock",
"BROWSER_NETWORK": "creatorhub_browser",
"GATEWAY_TOKEN": "0123456789abcdef",
}
)
self.assertEqual(config["listen"], ("", 8081))
external = load_config(
{
"LISTEN_ADDR": ":8081",
"DOCKER_SOCKET": "/var/run/docker.sock",
"BROWSER_NETWORK": "creatorhub_browser",
"GATEWAY_TOKEN": "0123456789abcdef",
"BROWSER_CDP_URL": "http://127.0.0.1:9222",
"BROWSER_CDP_TARGET_ID": "target_1",
}
)
self.assertEqual(external["external_cdp"]["url"], "http://127.0.0.1:9222")
self.assertEqual(external["external_cdp"]["target_id"], "target_1")
with self.assertRaises(ValueError):
load_config(
{
"GATEWAY_TOKEN": "0123456789abcdef",
"BROWSER_CDP_URL": "http://user:pass@127.0.0.1:9222",
}
)
tmp_mount = next(path for path in browser_tmpfs() if path.endswith("tmp"))
self.assertTrue(browser_tmpfs()[tmp_mount].startswith("rw,"))
self.assertTrue(
valid_douyin_url(
"https://www.douyin.com/aweme/v1/web/user/profile/self/?aid=6383&device_platform=webapp"
)
)
self.assertTrue(
valid_douyin_url(
"https://www.douyin.com/aweme/v1/web/user/profile/other/?aid=6383&device_platform=webapp&user_id=2328120603967913"
)
)
self.assertTrue(
valid_douyin_url(
"https://www.douyin.com/aweme/v1/web/user/profile/other/?aid=6383&device_platform=webapp&sec_user_id=MS4wLjABAAAA9f_a7k0bzVizLYXlpC7R61EIaqJ8Ordug7yp7AB8fGKuuF8Fzqk5_DM-eutXnPIK"
)
)
self.assertFalse(
valid_douyin_url(
"https://www.douyin.com/aweme/v1/web/user/profile/other/?aid=6383&device_platform=webapp&user_id=1&count=20"
)
)
self.assertFalse(valid_douyin_url("https://www.douyin.com.evil/"))
self.assertTrue(is_douyin_url("https://www.douyin.com/video/123"))
self.assertFalse(is_douyin_url("https://www.douyin.com.evil/video/123"))
def test_external_cdp_is_virtual_and_generation_bound(self) -> None:
external = {
"url": "http://127.0.0.1:9222",
"target_id": "target_1",
"alias": "local-cdp",
"runtime_id": "0" * 64,
"network_id": "local-cdp",
"binding_version": 1,
}
gateway = Gateway(
cast(DockerClient, FakeDocker([])),
"creatorhub_browser",
"0123456789abcdef",
"gateway",
external_cdp=external,
)
self.assertEqual(gateway.list_browsers()[0]["state"], "external")
generation = {
"binding_version": 1,
"runtime_id": "0" * 64,
"network_id": "local-cdp",
}
gateway._require_douyin_generation("local-cdp", generation)
with self.assertRaises(RequestError):
gateway._require_douyin_generation(
"local-cdp", {**generation, "binding_version": 2}
)
def test_http_routes_and_body_validation(self) -> None:
handler = gateway_module.GatewayHandler.__new__(gateway_module.GatewayHandler)
gateway = Mock()
gateway.list_browsers.return_value = []
gateway.douyin_identity.return_value = {"uid": "123"}
gateway.douyin_login_qr.return_value = {
"content_type": "image/png",
"body_base64": "cG5n",
"qr_detected": True,
}
gateway.douyin_action.return_value = {"status": "succeeded"}
gateway.douyin_message_history.return_value = {"status": "succeeded"}
gateway.poll_douyin_events.return_value = []
server = Mock()
server.gateway = gateway
cast(Any, handler).server = server
cast(Any, handler).server_as_gateway = lambda: server
self.assertEqual(handler._route("GET", "/v1/browsers", {}, {}), [])
self.assertEqual(
handler._route("POST", "/v1/browsers", {}, {}),
(201, gateway.create.return_value),
)
handler._route(
"DELETE",
"/v1/browsers/safe",
{},
{"binding_version": 1, "runtime_id": "a" * 64, "network_id": ""},
)
gateway.remove.assert_called_once_with(
"safe", {"binding_version": 1, "runtime_id": "a" * 64, "network_id": ""}
)
handler._route("POST", "/v1/browsers/safe/start", {}, {})
handler._route("POST", "/v1/browsers/safe/stop", {}, {})
handler._route("POST", "/v1/browsers/safe/proxy", {}, {})
with self.assertRaises(RequestError):
handler._route("POST", "/v1/browsers/safe/douyin/cookies", {}, {})
handler._route("POST", "/v1/browsers/safe/douyin/get", {}, {})
handler._route("POST", "/v1/browsers/safe/douyin/identity", {}, {})
self.assertEqual(
handler._route("POST", "/v1/browsers/safe/douyin/login-qr", {}, {}),
gateway.douyin_login_qr.return_value,
)
gateway.douyin_login_qr.assert_called_once_with("safe", {})
handler._route("POST", "/v1/browsers/safe/douyin/action", {}, {})
self.assertEqual(
handler._route("POST", "/v1/browsers/safe/douyin/messages", {}, {}),
{"status": "succeeded"},
)
gateway.douyin_message_history.assert_called_once_with("safe", {})
self.assertEqual(
handler._route("GET", "/v1/browsers/safe/douyin/events", {}, {}), []
)
handler._route("POST", "/v1/browsers/safe/douyin/events", {}, {})
handler._route("DELETE", "/v1/browsers/safe/douyin/events", {}, {})
with self.assertRaises(RequestError):
handler._route("GET", "/v1/unknown", {}, {})
cast(Any, handler).headers = {"Content-Length": "7"}
cast(Any, handler).rfile = io.BytesIO(b'{"x":1}')
self.assertEqual(handler._body(), {"x": 1})
cast(Any, handler).headers = {}
with self.assertRaises(RequestError):
handler._body()
test_token = "x" * 16
cast(Any, handler).headers = {"Authorization": f"Bearer {test_token}"}
server.gateway.token = test_token
self.assertTrue(handler._authorized())
handler._respond = Mock()
handler._handle_exception("/v1", RequestError("bad", 400))
handler._handle_exception("/v1", ValueError("bad"))
self.assertEqual(handler._respond.call_count, 2)
def test_get_event_route_reads_generation_body(self) -> None:
handler = gateway_module.GatewayHandler.__new__(gateway_module.GatewayHandler)
cast(Any, handler).path = "/v1/browsers/safe/douyin/events?wait=1"
cast(Any, handler).headers = {"Content-Length": "67"}
cast(Any, handler).rfile = io.BytesIO(
b'{"binding_version":1,"runtime_id":"runtime","network_id":"network"}'
)
cast(Any, handler)._authorized = lambda: True
cast(Any, handler)._route = Mock(return_value=[])
cast(Any, handler)._respond = Mock()
handler._dispatch("GET")
cast(Any, handler)._route.assert_called_once_with(
"GET",
"/v1/browsers/safe/douyin/events",
{"wait": ["1"]},
{"binding_version": 1, "runtime_id": "runtime", "network_id": "network"},
)
def test_validation_boundaries(self) -> None:
self.assertEqual(proxy_port("http://docker-gateway:1234"), 1234)
self.assertTrue(has_control("bad\nvalue"))
exit_value = parse_proxy_exit(
{"protocol": "http", "host": "proxy", "port": 8080}
)
validate_proxy_exit(exit_value)
with self.assertRaises(RequestError):
validate_proxy_exit(
parse_proxy_exit({"protocol": "ftp", "host": "proxy", "port": 21})
)
with self.assertRaises(RequestError):
validate_proxy_exit(ProxyExit("http", "proxy", 0))
generation = {
"binding_version": 1,
"runtime_id": "a" * 64,
"network_id": "b" * 64,
"network_exit_id": "exit",
}
self.assertTrue(valid_douyin_generation(generation))
self.assertFalse(
valid_douyin_generation({**generation, "binding_version": True})
)
self.assertTrue(valid_account_key_query({"account": ["account"]}, "account"))
self.assertFalse(valid_account_key_query({"account": ["bad key"]}, "account"))
self.assertTrue(numeric_cursor(["0"]))
self.assertFalse(numeric_cursor(["-1"]))
restore = {
"binding_version": 1,
"runtime_id": "a" * 64,
"network_id": "b" * 64,
"network_exit_id": "exit",
"network_exit": {"protocol": "http", "host": "proxy", "port": 8080},
}
validate_proxy_restore(restore, "safe")
with self.assertRaises(RequestError):
validate_proxy_restore({**restore, "network_exit_id": ""}, "safe")
with self.assertRaises(RequestError):
validate_proxy_restore({"network_exit_id": ""}, "safe")
with self.assertRaises(RequestError):
validate_create({"alias": "safe", "unknown": True})
with self.assertRaises(RequestError):
validate_create(
{
"alias": "safe",
"name": "Safe",
"image": "bad image",
"volume": "safe",
"binding_version": 1,
"cmd": ["about:blank"],
}
)
with self.assertRaises(ValueError):
load_config({"GATEWAY_TOKEN": "short"})
self.assertFalse(valid_douyin_url("http://www.douyin.com/video/1"))
self.assertFalse(valid_douyin_url("https://www.douyin.com/unknown"))
with self.assertRaises(RequestError):
decode_generation(
{"binding_version": 1, "runtime_id": "a" * 64}, True, True
)
def test_list_keeps_other_environments_when_one_network_is_missing(self) -> None:
class PartialDocker(FakeDocker):
def container_network_address(
self, container_id: str, network_id: str
) -> str:
if container_id == "broken":
raise GenerationConflict("network attachment disappeared")
return "192.0.2.10"
containers = [
{
"Id": "broken",
"Labels": {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "broken-account",
BINDING_VERSION_LABEL: "1",
NETWORK_EXIT_LABEL: "",
NETWORK_ID_LABEL: "b" * 64,
},
"State": "running",
"Status": "Up",
},
{
"Id": "healthy",
"Labels": {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "healthy-account",
BINDING_VERSION_LABEL: "1",
NETWORK_EXIT_LABEL: "",
NETWORK_ID_LABEL: "c" * 64,
},
"State": "running",
"Status": "Up",
},
]
result = Gateway(
cast(DockerClient, PartialDocker(containers)),
"creatorhub_browser",
"0123456789abcdef",
"gateway",
).list_browsers()
self.assertEqual(
[item["alias"] for item in result], ["broken-account", "healthy-account"]
)
self.assertEqual(result[0]["endpoint"], "")
self.assertIn("error", result[0])
self.assertEqual(result[1]["endpoint"], "http://192.0.2.10:9222")
def test_container_network_address(self) -> None:
docker = DockerClient("/var/run/docker.sock")
cast(Any, docker).request = lambda method, path: DockerResponse(
200,
"OK",
json.dumps(
{
"NetworkSettings": {
"Networks": {
"tenant": {
"NetworkID": "network",
"IPAddress": "192.0.2.20",
}
}
}
}
).encode(),
)
self.assertEqual(
docker.container_network_address("container", "network"), "192.0.2.20"
)
cast(Any, docker).request = lambda method, path: DockerResponse(
200, "OK", b'{"NetworkSettings":{"Networks":{}}}'
)
with self.assertRaises(GenerationConflict):
docker.container_network_address("container", "network")
def test_list_browsers_and_json(self) -> None:
docker = FakeDocker(
[
{
"Id": "container-id",
"Labels": {
"io.creatorhub.managed": "true",
"io.creatorhub.runtime-id": "safe-account",
"io.creatorhub.binding-version": "2",
"io.creatorhub.network-exit-id": "",
"io.creatorhub.network-id": "b" * 64,
},
"State": "running",
"Status": "Up 1 second",
}
]
)
gateway = Gateway(
cast(DockerClient, docker),
"creatorhub_browser",
"0123456789abcdef",
"gateway",
)
self.assertEqual(gateway.list_browsers()[0]["id"], "container-id")
self.assertEqual(
gateway.list_browsers()[0]["endpoint"], "http://192.0.2.10:9222"
)
self.assertEqual(
json_bytes({"text": "中文"}), b'{"text":"\xe4\xb8\xad\xe6\x96\x87"}'
)
class CDPTests(unittest.TestCase):
def test_connect_response_headers_are_drained(self) -> None:
connection = ChunkSocket(
[b"HTTP/1.1 200 Connection Established\r\n", b"X-Proxy: value\r\n\r\nTLS"]
)
self.assertEqual(_read_status(cast(socket.socket, connection)), 200)
def test_command_queues_events_for_wait_event(self) -> None:
socket_ = FakeSocket(
[
{
"method": "Page.frameNavigated",
"params": {"frame": {"id": "frame-1"}},
},
{"id": 1, "result": {}},
]
)
connection = CDPConnection(cast(websocket.WebSocket, socket_))
self.assertEqual(connection.command("Page.enable"), {})
event = connection.wait_event(
"Page.frameNavigated", lambda params: params["frame"]["id"] == "frame-1"
)
self.assertEqual(event["method"], "Page.frameNavigated")
self.assertTrue(socket_.sent)
def test_eof_status_is_a_failure(self) -> None:
class Closed:
def recv(self, size: int) -> bytes:
del size
return b""
with self.assertRaises(OSError):
_read_status(cast(socket.socket, Closed()))
def test_expression_markers_and_challenge(self) -> None:
expression = im_expression({"text": "hello EXPECTED_UID_VALUE"}, "123")
self.assertIn("hello EXPECTED_UID_VALUE", expression)
self.assertIn(
"https://www.douyin.com", action_expression({"action": "like_work"})
)
self.assertEqual(detect_challenge(429, "captcha"), "")
self.assertEqual(detect_challenge(412, ""), "captcha")
def test_notification_details_normalize_safe_targets(self) -> None:
notice = {
"nid_str": "9007199254740993",
"user_id": "99491952055",
"create_time": 1700000000,
"aweme_id": "123456",
"comment": {
"from_user": [{"uid": "7654321"}],
"comment": {"cid_str": "987654", "user": {"uid": "7654321"}},
},
}
self.assertEqual(
normalize_notice(notice),
{
"event_key": "9007199254740993",
"event_type": "comment",
"interactor_uid": "7654321",
"comment_id": "987654",
"work_id": "123456",
"platform_event_at": "2023-11-14T22:13:20+00:00",
},
)
favorite = {"nid_str": "7", "favorite": {"from_user": [{"uid": "1"}]}}
self.assertIsNone(normalize_notice(favorite))
def test_notification_detail_retries_partial_response(self) -> None:
details = [
{"nid_str": "1", "user_id": "123"},
{"nid_str": "2", "user_id": "123"},
]
payload = json.dumps({"status_code": 0, "notice_list_v2": details})
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.uid = "123"
subscription.connection = cast(
CDPConnection,
FakeConnection(
[
json.dumps(
{
"status": 200,
"body": json.dumps(
{"status_code": 0, "notice_list_v2": details[:1]}
),
}
),
json.dumps({"status": 200, "body": payload}),
]
),
)
subscription._get_connection = lambda: subscription.connection
with patch.object(douyin_module.time, "sleep"):
self.assertEqual(len(subscription._details(["1", "2"])), 2)
def test_notification_detail_rejects_unexpected_id(self) -> None:
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.uid = "123"
subscription.connection = cast(
CDPConnection,
FakeConnection(
[
json.dumps(
{
"status": 200,
"body": json.dumps(
{
"status_code": 0,
"notice_list_v2": [
{"nid_str": "9", "user_id": "123"}
],
}
),
}
)
]
),
)
subscription._get_connection = lambda: subscription.connection
with self.assertRaises(DouyinError):
subscription._details(["1"])
class BrowserCDP:
def __init__(self, values: list[object]) -> None:
self.values = list(values)
self.commands: list[tuple[str, dict | None]] = []
self.events: list[str] = []
self.expressions: list[str] = []
self.closed = False
def command(self, method: str, params: dict | None = None) -> dict:
self.commands.append((method, params))
if method == "Page.navigate":
return {"frameId": "frame-1"}
return {}
def wait_event(self, method: str, predicate: object, timeout: float = 15.0) -> dict:
del predicate, timeout
self.events.append(method)
return {"method": method}
def evaluate(self, expression: str) -> object:
self.expressions.append(expression)
if not self.values:
raise DouyinError("fake CDP value exhausted")
return self.values.pop(0)
def close(self) -> None:
self.closed = True
class FakeHTTPResponse:
def __init__(self, status: int, body: bytes) -> None:
self.status = status
self.body = body
def read(self, limit: int = -1) -> bytes:
del limit
return self.body
class FakeHTTPConnection:
def __init__(self, response: FakeHTTPResponse) -> None:
self.response = response
self.requested: list[tuple[str, str]] = []
self.closed = False
def request(
self,
method: str,
path: str,
body: bytes | None = None,
headers: dict[str, str] | None = None,
) -> None:
del body, headers
self.requested.append((method, path))
def getresponse(self) -> FakeHTTPResponse:
return self.response
def close(self) -> None:
self.closed = True
class BrowserTests(unittest.TestCase):
def _with_connection(self, browser: DouyinBrowser, connection: BrowserCDP) -> None:
@contextmanager
def bound(alias: str):
del alias
yield connection
cast(Any, browser).connection = bound
def test_browser_fetch_uses_manually_logged_session(self) -> None:
cdp = BrowserCDP(
[
"https://www.douyin.com",
{"status": 200, "body": "{}", "too_large": False},
]
)
browser = DouyinBrowser()
self._with_connection(browser, cdp)
response = browser.get(
"safe", "https://www.douyin.com/aweme/v1/web/user/profile/self/?aid=6383"
)
self.assertEqual(response.status, 200)
self.assertNotIn("Network.setCookies", [method for method, _ in cdp.commands])
def test_login_qr_captures_a_browser_screen_without_credentials(self) -> None:
screenshot = base64.b64encode(b"png-bytes").decode("ascii")
cdp = Mock()
cdp.evaluate.side_effect = [
True,
{"origin": "https://www.douyin.com", "qr_detected": True},
]
cdp.command.side_effect = [
{"frameId": "frame-1"},
{"data": screenshot},
]
browser = DouyinBrowser()
self._with_connection(browser, cast(BrowserCDP, cdp))
with patch.object(douyin_module.time, "sleep"):
response = browser.login_qr("safe")
self.assertEqual(response.content_type, "image/png")
self.assertEqual(response.body_base64, screenshot)
self.assertTrue(response.qr_detected)
self.assertEqual(
[call.args[0] for call in cdp.command.call_args_list],
["Page.navigate", "Page.captureScreenshot"],
)
self.assertFalse(any("cookie" in expression.lower() for expression in cdp.evaluate.call_args.args))
def test_connect_selects_configured_target_from_large_mixed_list(self) -> None:
targets = [
{"type": "service", "url": "http://127.0.0.1:9222/json"} for _ in range(40)
]
targets.extend(
[
{
"id": "other",
"type": "page",
"url": "https://www.douyin.com/video/1",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/1",
},
{
"id": "selected",
"type": "page",
"url": "https://www.douyin.com/user/self",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/2",
},
]
)
http = FakeHTTPConnection(FakeHTTPResponse(200, json.dumps(targets).encode()))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
patch.object(
douyin_module.websocket,
"create_connection",
return_value=FakeSocket([]),
),
):
connection = DouyinBrowser(
lambda alias: "http://127.0.0.1:9222", target_id="selected"
)._connect("safe")
self.assertIsInstance(connection, CDPConnection)
def test_media_download_is_browser_mediated_and_bounded(self) -> None:
payload = base64.b64encode(b"video-bytes").decode("ascii")
cdp = BrowserCDP(
[
{"url": "https://www.douyin.com/video/123", "readyState": "complete"},
{"status": 200, "content_type": "video/mp4", "body": payload},
]
)
browser = DouyinBrowser()
self._with_connection(browser, cdp)
response = browser.get_media("safe", "https://www.douyin.com/video/123")
self.assertEqual(response.status, 200)
self.assertEqual(response.content_type, "video/mp4")
self.assertEqual(base64.b64decode(response.body_base64), b"video-bytes")
self.assertIn("Page.navigate", [method for method, _ in cdp.commands])
self.assertIn("querySelectorAll", cdp.expressions[-1])
self.assertIn("uuu_265.mp4", cdp.expressions[-1])
def test_connect_identity_and_actions(self) -> None:
target = [
{
"type": "page",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/1",
}
]
http = FakeHTTPConnection(FakeHTTPResponse(200, json.dumps(target).encode()))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
patch.object(
douyin_module.websocket,
"create_connection",
return_value=FakeSocket([]),
),
):
connection = DouyinBrowser(lambda alias: "http://127.0.0.1:9222")._connect(
"safe"
)
self.assertIsInstance(connection, CDPConnection)
self.assertEqual(http.requested[0], ("GET", "/json/list"))
expression = install_expression("safe", "123")
self.assertIn("safe", expression)
self.assertIn("old?.dispose?.()", expression)
self.assertEqual(
notice_ids(
{
"service": 20313,
"payload": json.dumps(
{"notices": [{"notice_id_str": "1", "effect_groups": [960]}]}
),
}
),
["1"],
)
self.assertEqual(
notice_ids(
{
"service": 20003,
"payload": json.dumps({"notice_type": 45, "notice_id_str": "2"}),
}
),
["2"],
)
browser = DouyinBrowser()
cast(Any, browser).identity = lambda alias, expected: {"uid": expected}
cast(Any, browser)._evaluate = lambda *args: {
"status": "succeeded",
"action": "followed",
}
preview = browser.action(
"safe", "123", "dm", "456", text="hello", confirm=False
)
self.assertEqual(preview["action"], "preview")
self.assertEqual(
browser.action("safe", "123", "like_work", work_id="789", confirm=False)[
"action"
],
"preview",
)
with self.assertRaises(DouyinError):
browser.action("safe", "123", "dm", "456", text=" ", confirm=False)
with self.assertRaises(DouyinError):
browser.action("safe", "123", "follow", "bad", confirm=False)
def test_browser_queue_is_retained_until_ack(self) -> None:
wait = wait_expression("alpha")
self.assertIn("delivered", wait)
self.assertNotIn("splice(0)", wait)
ack = ack_expression("__creatorhub_notice_sub_alpha", ["browser-1"])
self.assertIn("browser-1", ack)
self.assertIn("__creatorhub_notice_sub_alpha", ack)
self.assertIn(
"crypto.randomUUID", douyin_module.install_expression("alpha", "123")
)
def test_listener_start_retries_until_runtime_ready(self) -> None:
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.alias = "safe"
subscription.uid = "123"
subscription.stopped = threading.Event()
subscription.stopped.wait = Mock(return_value=False)
subscription._open_listener = Mock(
side_effect=[
DouyinError("SDK_NOT_READY"),
(Mock(), "2026-09-14T18:00:00+00:00"),
]
)
with patch.object(douyin_module.time, "monotonic", side_effect=[0.0, 1.0]):
connection, boundary = subscription._open_listener_until_ready()
self.assertEqual(boundary, "2026-09-14T18:00:00+00:00")
self.assertIsNotNone(connection)
self.assertEqual(subscription._open_listener.call_count, 2)
subscription.stopped.wait.assert_called_once_with(0.5)
def test_subscription_receipts_are_replayed_until_ack(self) -> None:
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.uid = "123"
subscription.queue = deque()
subscription.condition = threading.Condition()
subscription.stopped = threading.Event()
subscription._put({"kind": "notice", "notice": {"event_key": "1"}})
first = subscription.poll(10, 0)
second = subscription.poll(10, 0)
self.assertEqual(first, second)
self.assertTrue(first[0]["notice"]["gateway_received_at"].endswith("+00:00"))
subscription.ack([first[0]["delivery_id"]])
self.assertEqual(subscription.poll(10, 0), [])
def test_subscription_connection_failure_requests_recovery(self) -> None:
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.alias = "safe"
subscription.browser = Mock()
subscription.browser._connect.side_effect = DouyinError("browser unavailable")
subscription._put = Mock()
subscription._request_recovery = Mock()
subscription._browser_inflight_lock = threading.Lock()
subscription._browser_inflight = set()
subscription._process_push_async({"delivery_id": "delivery-1"}, 0, False)
subscription._request_recovery.assert_called_once_with()
def test_subscription_detail_failure_does_not_discard_siblings(self) -> None:
bad = json.dumps(
{
"status": 200,
"body": json.dumps(
{
"status_code": 0,
"notice_list_v2": [{"nid_str": "9", "user_id": "123"}],
}
),
}
)
good = json.dumps(
{
"status": 200,
"body": json.dumps(
{
"status_code": 0,
"notice_list_v2": [
{
"nid_str": "2",
"user_id": "123",
"follow": {"from_user": [{"uid": "7"}]},
}
],
}
),
}
)
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.uid = "123"
subscription.queue = deque()
subscription.condition = threading.Condition()
subscription.stopped = threading.Event()
subscription.connection = cast(CDPConnection, FakeConnection([bad, bad, good]))
subscription._get_connection = lambda: subscription.connection
subscription._handle(
{
"kind": "push",
"service": 20313,
"payload": json.dumps(
{
"notices": [
{"notice_id_str": "1", "effect_groups": [960]},
{"notice_id_str": "2", "effect_groups": [960]},
]
}
),
}
)
events = subscription.poll(10, 0)
self.assertEqual(events[0]["kind"], "error")
self.assertEqual(events[0]["event_key"], "1")
self.assertEqual(events[1]["notice"]["event_key"], "2")
def test_subscription_manager_and_queue(self) -> None:
event = {"kind": "open"}
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.uid = "123"
subscription.queue = deque()
subscription.condition = __import__("threading").Condition()
subscription.stopped = __import__("threading").Event()
subscription._handle(event)
first = subscription.poll(10, 0)
self.assertEqual(first[0]["kind"], "open")
self.assertTrue(first[0]["delivery_id"])
subscription.queue = deque([{} for _ in range(1000)])
subscription._put({"kind": "new"})
self.assertEqual(subscription.queue[0]["kind"], "error")
subscription.stopped.set()
overflow = subscription.poll(10, 0)
self.assertEqual(overflow[0]["kind"], "error")
self.assertEqual(overflow[0]["reason"], "notification queue overflow")
self.assertTrue(overflow[0]["delivery_id"])
self.assertEqual(len(overflow), 10)
self.assertEqual(len(subscription.queue), 1000)
subscription.ack([overflow[0]["delivery_id"]])
self.assertEqual(len(subscription.queue), 999)
self.assertEqual(subscription.queue[0], {})
browser = DouyinBrowser()
fake = Mock()
fake.poll.return_value = [{"kind": "notice"}]
with patch.object(douyin_module, "DouyinSubscription", return_value=fake):
manager = SubscriptionManager(browser)
self.assertTrue(manager.start("safe", "123")["connected"])
self.assertEqual(manager.poll("safe", 1, 0), [{"kind": "notice"}])
manager.stop("safe")
manager.close()
with self.assertRaises(DouyinError):
manager.poll("safe", 1, 0)
class ProxyTests(unittest.TestCase):
def test_chunked_request_body_is_decoded_and_forwarded_with_length(self) -> None:
request = ChunkSocket(
[
(
b"POST http://example.test/a HTTP/1.1\r\nHost: example.test\r\n"
b"Transfer-Encoding: chunked\r\n\r\n2\r\nab\r\n3;part=x\r\ncde\r\n0\r\nX-Trailer: yes\r\n\r\n"
)
]
)
head, body = _read_request(cast(socket.socket, request))
self.assertEqual(body, b"abcde")
proxy = MemoryProxy.__new__(MemoryProxy)
proxy.exit = ProxyExit("socks5", "proxy", 1080)
upstream = ChunkSocket([b""])
proxy.dial = lambda target, timeout=20.0: cast(socket.socket, upstream)
with patch.object(proxy_module, "_copy_until_close"):
proxy.forward_http(
cast(socket.socket, ChunkSocket([])),
"POST",
"http://example.test/a",
_parse_request(head)[2],
body,
)
sent = upstream.sent[0].decode("iso-8859-1")
self.assertIn("Content-Length: 5", sent)
self.assertNotIn("Transfer-Encoding:", sent)
def test_registry_generation_and_shutdown(self) -> None:
registry = ProxyRegistry()
url, undo = registry.configure(
"safe", 1, "127.0.0.1", 0, ProxyExit("http", "127.0.0.1", 8080), "network-1"
)
port = int(url.rsplit(":", 1)[1])
self.assertFalse(registry.ready("safe", port, "container-1", "network-1"))
self.assertTrue(registry.bind("safe", 1, url, "container-1", "network-1"))
self.assertTrue(registry.ready("safe", port, "container-1", "network-1"))
self.assertFalse(registry.remove("safe", 1, "container-2", "network-1"))
undo()
registry.close()
def test_memory_proxy_can_close(self) -> None:
proxy = MemoryProxy(
"safe", 1, "127.0.0.1", 0, ProxyExit("http", "127.0.0.1", 8080), "network"
)
self.assertGreater(proxy.listener.getsockname()[1], 0)
proxy.close()
def test_proxy_request_parser_and_copy_helpers(self) -> None:
request = ChunkSocket(
[
b"POST http://example.test/a HTTP/1.1\r\nContent-Length: 3\r\nHost: example.test\r\n\r\nabc"
]
)
head, body = _read_request(cast(socket.socket, request))
self.assertEqual(body, b"abc")
method, target, headers = _parse_request(head)
self.assertEqual((method, target), ("POST", "http://example.test/a"))
self.assertEqual(headers[0], ("Content-Length", "3"))
source = ChunkSocket([b"one", b"", b"ignored"])
destination = ChunkSocket([])
_copy_until_close(cast(socket.socket, source), cast(socket.socket, destination))
self.assertEqual(b"".join(destination.sent), b"one")
self.assertEqual(
_recv_exact(cast(socket.socket, ChunkSocket([b"ab", b"cd"])), 4), b"abcd"
)
self.assertTrue(_is_ipv4("127.0.0.1"))
self.assertFalse(_is_ipv4("host.example"))
with self.assertRaises(ValueError):
_parse_request(b"BROKEN\r\n\r\n")
def test_connect_payload_in_same_read_is_preserved(self) -> None:
class GreedySocket(ChunkSocket):
def recv(self, size: int) -> bytes:
del size
return self.chunks.pop(0) if self.chunks else b""
upstream = GreedySocket([b"HTTP/1.1 200 OK\r\nX-Test: yes\r\n\r\nTLS"])
with patch.object(proxy_module, "_open_host", return_value=upstream):
result = _dial_http_proxy(
ProxyExit("http", "proxy", 8080), "target:443", 1.0
)
self.assertEqual(_recv_exact(cast(socket.socket, result), 3), b"TLS")
result.close()
def test_http_and_socks_handshakes(self) -> None:
http_socket = ChunkSocket([b"HTTP/1.1 200 Connection Established\r\n\r\n"])
with patch.object(proxy_module, "_open_host", return_value=http_socket):
result = _dial_http_proxy(
ProxyExit("http", "proxy", 8080, "u", "c"), "target:443", 1.0
)
self.assertIs(result, http_socket)
self.assertIn(b"Proxy-Authorization: Basic dTpj", http_socket.sent[0])
socks4_socket = ChunkSocket([b"\x00\x5a\x00\x00\x00\x00\x00\x00"])
with patch.object(proxy_module, "_open_host", return_value=socks4_socket):
self.assertIs(
_dial_socks4(ProxyExit("socks4", "proxy", 1080), "127.0.0.1:80", 1.0),
socks4_socket,
)
socks5_socket = ChunkSocket(
[b"\x05\x00", b"\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x50"]
)
with patch.object(proxy_module, "_open_host", return_value=socks5_socket):
self.assertIs(
_dial_socks5(ProxyExit("socks5", "proxy", 1080), "127.0.0.1:80", 1.0),
socks5_socket,
)
with self.assertRaises(OSError):
_dial_socks5(ProxyExit("socks5", "proxy", 1080), "host:0", 1.0)
def test_proxy_forward_and_tunnel_paths(self) -> None:
proxy = MemoryProxy.__new__(MemoryProxy)
proxy.exit = ProxyExit("socks5", "proxy", 1080)
upstream = ChunkSocket([b"reply", b""])
client = ChunkSocket([])
cast(Any, proxy).dial = lambda target, timeout=20.0: cast(
socket.socket, upstream
)
with patch.object(proxy_module, "_copy_until_close") as copy:
proxy.forward_http(
cast(socket.socket, client),
"GET",
"http://example.test/path?q=1",
[("Host", "example.test")],
b"",
)
copy.assert_called_once()
self.assertIn(b"GET /path?q=1 HTTP/1.1", upstream.sent[0])
proxy.exit = ProxyExit("http", "proxy", 8080)
upstream = ChunkSocket([b""])
cast(Any, proxy).dial = lambda target, timeout=20.0: cast(
socket.socket, upstream
)
with (
patch.object(proxy_module, "_open_host", return_value=upstream),
patch.object(proxy_module, "_copy_until_close"),
):
proxy.forward_http(
cast(socket.socket, client), "GET", "http://example.test/", [], b""
)
self.assertIn(b"GET http://example.test/ HTTP/1.1", upstream.sent[0])
class ChunkSocket:
def __init__(self, chunks: list[bytes]) -> None:
self.chunks = list(chunks)
self.sent: list[bytes] = []
def recv(self, size: int) -> bytes:
if not self.chunks:
return b""
chunk = self.chunks.pop(0)
if len(chunk) <= size:
return chunk
self.chunks.insert(0, chunk[size:])
return chunk[:size]
def sendall(self, data: bytes) -> None:
self.sent.append(data)
def send(self, data: bytes) -> None:
self.sent.append(data)
def settimeout(self, value: float | None) -> None:
del value
def close(self) -> None:
return None
class DockerClientTests(unittest.TestCase):
def test_digest_pull_preserves_digest_in_docker_query(self) -> None:
client = ScriptedDocker(
[
DockerResponse(404, "Not Found", b""),
DockerResponse(200, "OK", b""),
]
)
client.pull_if_missing("registry.example/repo@sha256:" + "a" * 64)
self.assertIn(
"fromImage=registry.example%2Frepo%40sha256%3A" + "a" * 64,
client.calls[1][1],
)
def test_image_ref_and_expected_statuses(self) -> None:
self.assertEqual(
split_image_ref("registry.example/repo:tag"),
("registry.example/repo", "tag"),
)
self.assertEqual(split_image_ref("repo@sha256:abc"), ("repo", "sha256:abc"))
client = ScriptedDocker(
[
DockerResponse(204, "No Content", b""),
DockerResponse(404, "Not Found", b""),
DockerResponse(500, "Error", b"failure"),
]
)
client.expect("POST", "/ok")
with self.assertRaises(FileNotFoundError):
client.expect("POST", "/missing")
with self.assertRaises(DockerError):
client.expect("POST", "/error")
self.assertEqual(tenant_network_name("creatorhub", "safe"), "creatorhub-safe")
def test_pull_and_managed_container_response_validation(self) -> None:
client = ScriptedDocker(
[
DockerResponse(404, "Not Found", b""),
DockerResponse(200, "OK", b"{}"),
]
)
client.pull_if_missing("repo:tag")
self.assertIn("/images/repo%3Atag/json", client.calls[0][1])
managed = {
"Id": "container-id",
"Config": {"Labels": {MANAGED_LABEL: "true", RUNTIME_ID_LABEL: "safe"}},
"NetworkSettings": {"Networks": {"tenant": {"NetworkID": "network-id"}}},
}
client = ScriptedDocker(
[DockerResponse(200, "OK", json.dumps(managed).encode())]
)
self.assertEqual(client.managed_container_state("safe")[0], "container-id")
unmanaged = {
**managed,
"Config": {"Labels": {MANAGED_LABEL: "false", RUNTIME_ID_LABEL: "safe"}},
}
client = ScriptedDocker(
[DockerResponse(200, "OK", json.dumps(unmanaged).encode())]
)
with self.assertRaises(UnmanagedContainer):
client.managed_container("safe")
def test_existing_network_generation_and_disconnect(self) -> None:
network = {
"Id": "network-id",
"Name": "creatorhub-safe",
"Driver": "bridge",
"Internal": False,
"Attachable": False,
"Ingress": False,
"Labels": {
MANAGED_LABEL: "true",
NETWORK_ROLE_LABEL: BROWSER_NETWORK_ROLE,
RUNTIME_ID_LABEL: "safe",
BINDING_VERSION_LABEL: "1",
},
"Containers": {
"gateway-id": {"Name": "gateway", "IPv4Address": "10.0.0.2/24"},
"runtime-id": {"Name": "runtime", "IPv4Address": "10.0.0.3/24"},
},
}
gateway = {
"Id": "gateway-id",
"Config": {"Labels": {GATEWAY_MEMBER_LABEL: "true"}},
}
client = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(network).encode()),
DockerResponse(200, "OK", json.dumps(gateway).encode()),
]
)
generation, addresses, exists = client.inspect_tenant_network(
"creatorhub", "safe", 1, "runtime-id", "gateway", "network-id"
)
self.assertTrue(exists)
self.assertTrue(generation.runtime_attached)
self.assertEqual(addresses["gateway-id"], "10.0.0.2/24")
self.assertEqual(generation.self_member, "gateway-id")
after = {
**network,
"Containers": {"gateway-id": network["Containers"]["gateway-id"]},
}
client = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(network).encode()),
DockerResponse(200, "OK", json.dumps(gateway).encode()),
DockerResponse(200, "OK", json.dumps(network).encode()),
DockerResponse(200, "OK", json.dumps(gateway).encode()),
DockerResponse(200, "OK", b""),
DockerResponse(200, "OK", json.dumps(after).encode()),
DockerResponse(200, "OK", json.dumps(gateway).encode()),
]
)
current, _, _ = client.inspect_tenant_network(
"creatorhub", "safe", 1, "runtime-id", "gateway", "network-id"
)
updated = client.disconnect_member(
"creatorhub", "safe", 1, "runtime-id", current, "runtime-id", "gateway"
)
self.assertFalse(updated.runtime_attached)
def test_new_network_is_generation_fenced(self) -> None:
empty = {
"Id": "network-id",
"Name": "creatorhub-safe",
"Driver": "bridge",
"Internal": False,
"Attachable": False,
"Ingress": False,
"Labels": {
MANAGED_LABEL: "true",
NETWORK_ROLE_LABEL: BROWSER_NETWORK_ROLE,
RUNTIME_ID_LABEL: "safe",
BINDING_VERSION_LABEL: "1",
},
"Containers": {},
}
connected = {
**empty,
"Containers": {
"runtime-id": {"Name": "runtime", "IPv4Address": "10.0.0.3/24"},
"gateway-id": {"Name": "gateway", "IPv4Address": "10.0.0.2/24"},
},
}
gateway = {
"Id": "gateway-id",
"Config": {"Labels": {GATEWAY_MEMBER_LABEL: "true"}},
}
client = ScriptedDocker(
[
DockerResponse(404, "Not Found", b""),
DockerResponse(201, "Created", b'{"Id":"network-id"}'),
DockerResponse(200, "OK", json.dumps(empty).encode()),
DockerResponse(200, "OK", b""),
DockerResponse(200, "OK", b""),
DockerResponse(200, "OK", json.dumps(connected).encode()),
DockerResponse(200, "OK", json.dumps(gateway).encode()),
]
)
generation, bind_host = client.ensure_tenant_network(
"creatorhub", "safe", "gateway", 1, "runtime-id"
)
self.assertTrue(generation.created)
self.assertEqual(bind_host, "10.0.0.2")
def test_alias_reservation_releases_only_its_generation(self) -> None:
gateway = {
"Id": "gateway-id",
"Image": "creatorhub/gateway:latest",
"Config": {"Labels": {MANAGED_LABEL: "true", GATEWAY_MEMBER_LABEL: "true"}},
}
reservation = {
"Id": "reservation-id",
"Config": {
"Labels": {
"io.creatorhub.alias-reservation": "true",
"io.creatorhub.reservation-generation": "reservation-generation",
RUNTIME_ID_LABEL: "safe",
}
},
}
client = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(gateway).encode()),
DockerResponse(201, "Created", b'{"Id":"reservation-id"}'),
DockerResponse(200, "OK", json.dumps(reservation).encode()),
DockerResponse(200, "OK", json.dumps(reservation).encode()),
DockerResponse(204, "No Content", b""),
DockerResponse(404, "Not Found", b""),
]
)
with patch.object(
docker_client_module,
"random_reservation_generation",
return_value="reservation-generation",
):
release = AliasReservationManager(client, "gateway").acquire("safe")
release()
self.assertTrue(any(call[0] == "DELETE" for call in client.calls))
def test_stale_alias_reservation_is_reclaimed(self) -> None:
gateway = {
"Id": "gateway-id",
"Image": "creatorhub/gateway:latest",
"Config": {"Labels": {MANAGED_LABEL: "true", GATEWAY_MEMBER_LABEL: "true"}},
}
stale = {
"Id": "stale-reservation-id",
"Config": {
"Labels": {
RESERVATION_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
RESERVATION_OWNER_LABEL: "old-gateway",
}
},
}
current = {
"Id": "current-reservation-id",
"Config": {
"Labels": {
RESERVATION_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
RESERVATION_OWNER_LABEL: "gateway",
"io.creatorhub.reservation-generation": "current-generation",
}
},
}
client = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(gateway).encode()),
DockerResponse(409, "Conflict", b""),
DockerResponse(200, "OK", json.dumps(stale).encode()),
DockerResponse(404, "Not Found", b""),
DockerResponse(204, "No Content", b""),
DockerResponse(404, "Not Found", b""),
DockerResponse(201, "Created", b'{"Id":"current-reservation-id"}'),
DockerResponse(200, "OK", json.dumps(current).encode()),
DockerResponse(200, "OK", json.dumps(current).encode()),
DockerResponse(204, "No Content", b""),
DockerResponse(404, "Not Found", b""),
]
)
with patch.object(
docker_client_module,
"random_reservation_generation",
return_value="current-generation",
):
release = AliasReservationManager(client, "gateway").acquire("safe")
release()
self.assertEqual(
client.calls[4][1],
"/containers/stale-reservation-id?force=1&v=0",
)
self.assertEqual(client.calls[6][0], "POST")
class ScriptedDocker(DockerClient):
def __init__(self, responses: list[DockerResponse]) -> None:
super().__init__("/dev/null")
self.responses = list(responses)
self.calls: list[tuple[str, str, object | None]] = []
def request(
self,
method: str,
path: str,
payload: object | None = None,
timeout: float = 30.0,
body_limit: int = 16 * 1024 * 1024,
) -> DockerResponse:
del timeout, body_limit
self.calls.append((method, path, payload))
if not self.responses:
raise AssertionError(f"unexpected Docker call: {method} {path}")
return self.responses.pop(0)
class ReservationStub:
def __init__(self) -> None:
self.released = 0
def acquire(self, alias: str):
del alias
def release() -> None:
self.released += 1
return release
class LockingReservationStub(ReservationStub):
def __init__(self) -> None:
super().__init__()
self.lock = threading.Lock()
def acquire(self, alias: str):
del alias
self.lock.acquire()
def release() -> None:
self.released += 1
self.lock.release()
return release
class LifecycleDocker(ScriptedDocker):
def ensure_tenant_network(
self, *args: object, **kwargs: object
) -> tuple[TenantNetworkGeneration, str]:
del args, kwargs
return TenantNetworkGeneration(
id="network-id", self_member="gateway-id"
), "10.0.0.2"
def inspect_tenant_network(
self, *args: object, **kwargs: object
) -> tuple[TenantNetworkGeneration, dict[str, str], bool]:
del args, kwargs
return (
TenantNetworkGeneration(
id="network-id",
name="creatorhub-safe",
self_member="gateway-id",
gateway_members=["gateway-id"],
runtime_attached=True,
),
{"gateway-id": "10.0.0.2/24"},
True,
)
class GatewayLifecycleTests(unittest.TestCase):
def test_server_tracks_daemon_request_threads_and_timeout(self) -> None:
server = gateway_module.GatewayHTTPServer(("127.0.0.1", 0), Mock())
try:
self.assertTrue(server.daemon_threads)
self.assertEqual(gateway_module.GatewayHandler.protocol_version, "HTTP/1.1")
finally:
server.server_close()
def test_network_setup_error_keeps_created_generation(self) -> None:
client = ScriptedDocker(
[
DockerResponse(404, "Not Found", b""),
DockerResponse(201, "Created", b'{"Id":"network-id"}'),
DockerResponse(500, "Error", b"inspect failed"),
]
)
with self.assertRaises(docker_client_module.NetworkSetupError) as caught:
client.ensure_tenant_network("creatorhub", "safe", "gateway", 1, "runtime")
self.assertEqual(caught.exception.generation.id, "network-id")
self.assertTrue(caught.exception.generation.created)
def test_network_cleanup_failure_returns_pending_contract(self) -> None:
docker = ScriptedDocker(
[
DockerResponse(404, "Not Found", b""),
DockerResponse(500, "Error", b"temporary"),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
with self.assertRaises(RequestError) as caught:
gateway.remove(
"safe",
{
"binding_version": 1,
"runtime_id": "runtime-not-found",
"network_id": "network-id",
},
)
self.assertEqual(caught.exception.status, 202)
self.assertEqual(caught.exception.network_id, "network-id")
self.assertEqual(str(caught.exception), "runtime_cleanup_pending")
def test_timed_out_action_retains_alias_ownership(self) -> None:
gateway = Gateway(
cast(DockerClient, Mock()), "creatorhub", "0123456789abcdef", "gateway"
)
gateway._claim_action("safe")
gateway._retain_action_ownership("safe")
with self.assertRaises(RequestError):
gateway._claim_action("safe")
def _input(self, stopped: bool = True) -> dict:
return {
"alias": "safe",
"name": "Safe",
"image": "creatorhub/browser:latest",
"cmd": ["about:blank"],
"volume": "creatorhub-safe",
"binding_version": 1,
"network_exit_id": "",
"stopped": stopped,
}
def test_concurrent_create_cannot_delete_the_winner(self) -> None:
class ConcurrentDocker(DockerClient):
def __init__(self) -> None:
super().__init__("/dev/null")
self.created = False
self.calls: list[tuple[str, str]] = []
def managed_container(self, alias: str) -> tuple[str, dict[str, str]]:
if not self.created:
raise FileNotFoundError(alias)
return "a" * 64, {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: alias,
BINDING_VERSION_LABEL: "1",
NETWORK_ID_LABEL: "",
}
def request(
self,
method: str,
path: str,
payload: object | None = None,
timeout: float = 30.0,
body_limit: int = 16 * 1024 * 1024,
) -> DockerResponse:
del payload, timeout, body_limit
self.calls.append((method, path))
if method == "GET" and path.startswith("/images/"):
return DockerResponse(200, "OK", b"{}")
if method == "POST" and path.startswith("/containers/create"):
if self.created:
return DockerResponse(409, "Conflict", b"")
self.created = True
return DockerResponse(
201, "Created", b'{"Id":"' + b"b" * 64 + b'"}'
)
raise AssertionError(f"unexpected Docker call: {method} {path}")
docker = ConcurrentDocker()
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, LockingReservationStub())
results: list[object] = []
def create() -> None:
try:
results.append(gateway.create(self._input()))
except (
AssertionError,
DockerError,
OSError,
RequestError,
ValueError,
) as exc:
results.append(exc)
threads = [threading.Thread(target=create) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertEqual(sum(isinstance(item, dict) for item in results), 1)
self.assertEqual(sum(isinstance(item, RequestError) for item in results), 1)
self.assertFalse(any(method == "DELETE" for method, _ in docker.calls))
def test_create_stopped_pulls_image_and_keeps_container(self) -> None:
docker = ScriptedDocker(
[
DockerResponse(200, "OK", b"{}"),
DockerResponse(404, "Not Found", b""),
DockerResponse(201, "Created", b'{"Id":"container-id"}'),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
reservations = ReservationStub()
gateway.reservations = cast(AliasReservationManager, reservations)
result = gateway.create(self._input())
self.assertEqual(result["id"], "container-id")
self.assertEqual(reservations.released, 1)
self.assertEqual(docker.calls[2][0], "POST")
def test_running_create_starts_container_and_network(self) -> None:
docker = LifecycleDocker(
[
DockerResponse(200, "OK", b"{}"),
DockerResponse(404, "Not Found", b""),
DockerResponse(201, "Created", b'{"Id":"container-id"}'),
DockerResponse(204, "No Content", b""),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
result = gateway.create(self._input(False))
self.assertEqual(result["network_id"], "network-id")
self.assertEqual(docker.calls[-1][0], "POST")
def test_change_state_and_remove_are_generation_fenced(self) -> None:
labels = {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
BINDING_VERSION_LABEL: "1",
NETWORK_ID_LABEL: "network-id",
}
inspected = {
"Id": "a" * 64,
"Config": {"Labels": labels},
"NetworkSettings": {"Networks": {}},
}
docker = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(inspected).encode()),
DockerResponse(204, "No Content", b""),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
gateway.change_state(
"safe",
"stop",
{
"binding_version": 1,
"runtime_id": "a" * 64,
"network_id": "network-id",
},
)
self.assertEqual(docker.calls[-1][0], "POST")
docker = LifecycleDocker(
[
DockerResponse(200, "OK", json.dumps(inspected).encode()),
DockerResponse(204, "No Content", b""),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
gateway._remove_network = lambda *args, **kwargs: None
gateway.remove(
"safe",
{
"binding_version": 1,
"runtime_id": "a" * 64,
"network_id": "network-id",
},
)
self.assertEqual(docker.calls[-1][0], "DELETE")
def test_remove_stopped_direct_container_without_network(self) -> None:
labels = {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
BINDING_VERSION_LABEL: "1",
NETWORK_ID_LABEL: "",
}
inspected = {
"Id": "a" * 64,
"Config": {"Labels": labels},
"NetworkSettings": {"Networks": {}},
}
docker = ScriptedDocker(
[
DockerResponse(200, "OK", json.dumps(inspected).encode()),
DockerResponse(404, "Not Found", b""),
DockerResponse(204, "No Content", b""),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
gateway.remove(
"safe",
{"binding_version": 1, "runtime_id": "a" * 64, "network_id": ""},
)
self.assertEqual(docker.calls[-1][0], "DELETE")
def test_remove_rejects_runtime_replacement(self) -> None:
labels = {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
BINDING_VERSION_LABEL: "1",
NETWORK_ID_LABEL: "",
}
inspected = {
"Id": "a" * 64,
"Config": {"Labels": labels},
"NetworkSettings": {"Networks": {}},
}
docker = ScriptedDocker(
[DockerResponse(200, "OK", json.dumps(inspected).encode())]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
with self.assertRaises(RequestError):
gateway.remove(
"safe",
{"binding_version": 1, "runtime_id": "b" * 64, "network_id": ""},
)
def test_create_failure_reconciles_unknown_container(self) -> None:
docker = ScriptedDocker(
[
DockerResponse(200, "OK", b"{}"),
DockerResponse(404, "Not Found", b""),
DockerResponse(500, "Error", b"failed"),
DockerResponse(404, "Not Found", b""),
]
)
gateway = Gateway(
cast(DockerClient, docker), "creatorhub", "0123456789abcdef", "gateway"
)
gateway.reservations = cast(AliasReservationManager, ReservationStub())
with self.assertRaises(RequestError):
gateway.create(self._input())
class AdditionalGatewayCoverageTests(unittest.TestCase):
def test_douyin_connect_rejects_bad_discovery(self) -> None:
browser = DouyinBrowser(lambda _: "https://browser:9222")
with self.assertRaises(DouyinError):
browser._connect("safe")
cases = [
(500, b"{}"),
(200, b"{}"),
(200, json.dumps([{"type": "service"}]).encode()),
(
200,
json.dumps(
[{"type": "page", "url": "https://www.douyin.com/1"}]
).encode(),
),
(
200,
json.dumps(
[
{
"type": "page",
"url": "https://www.douyin.com/1",
"webSocketDebuggerUrl": "http://browser/devtools/page/1",
}
]
).encode(),
),
]
for status, body in cases:
with self.subTest(status=status, body=body):
http = FakeHTTPConnection(FakeHTTPResponse(status, body))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
self.assertRaises(DouyinError),
):
browser._connect("safe")
targets = [
{
"type": "page",
"url": "https://www.douyin.com/1",
"webSocketDebuggerUrl": "ws://browser:9222/devtools/page/1",
},
{
"type": "page",
"url": "https://www.douyin.com/2",
"webSocketDebuggerUrl": "ws://browser:9222/devtools/page/2",
},
]
http = FakeHTTPConnection(FakeHTTPResponse(200, json.dumps(targets).encode()))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
self.assertRaises(DouyinError),
):
browser._connect("safe")
def test_douyin_fetch_identity_and_confirmed_actions(self) -> None:
browser = DouyinBrowser()
def bind(connection: BrowserCDP) -> None:
BrowserTests()._with_connection(browser, connection)
for result in (
{"too_large": True},
{"status": 302, "body": ""},
{"status": 200, "body": 1},
):
bind(BrowserCDP(["https://www.douyin.com", result]))
with self.assertRaises(DouyinError):
browser.get("safe", "https://www.douyin.com/a")
cast(Any, browser).get = lambda alias, target: BrowserResponse(200, "not-json")
with self.assertRaises(DouyinError):
browser.identity("safe")
cast(Any, browser).get = lambda alias, target: BrowserResponse(
200, json.dumps({"status_code": 0, "user": {"uid": "1", "sec_uid": "sec"}})
)
self.assertEqual(browser.identity("safe")["uid"], "1")
cast(Any, browser).get = lambda alias, target: BrowserResponse(
200,
json.dumps(
{
"status_code": 0,
"extra": {"now": 1789401629000},
"user": {"uid": "1", "sec_uid": "sec"},
}
),
)
self.assertTrue(browser.identity("safe")["platform_now"].startswith("2026-"))
cast(Any, browser).get = lambda alias, target: BrowserResponse(
403, json.dumps({"status_code": 0, "user": {"uid": "1", "sec_uid": "sec"}})
)
with self.assertRaises(DouyinError):
browser.identity("safe")
cast(Any, browser).identity = lambda alias, expected_uid=None: {
"uid": expected_uid or "1"
}
cast(Any, browser)._evaluate = lambda alias, expression: {
"status": 200,
"action": "sent",
}
bind(BrowserCDP(["https://www.douyin.com", {}]))
self.assertEqual(
browser.action("safe", "1", "follow", "2", confirm=True)["action"], "sent"
)
bind(BrowserCDP(["https://www.douyin.com", {}]))
self.assertEqual(
browser.action("safe", "1", "dm", "2", text="hello", confirm=True)[
"status"
],
200,
)
cast(Any, browser)._evaluate = lambda alias, expression: "bad"
bind(BrowserCDP(["https://www.douyin.com", "bad"]))
with self.assertRaises(DouyinError):
browser.action("safe", "1", "follow", "2", confirm=True)
def test_gateway_and_proxy_validation_edges(self) -> None:
self.assertEqual(
parse_proxy_exit({"protocol": "http", "host": "", "port": 80}).host, ""
)
with self.assertRaises(RequestError):
validate_proxy_exit(
parse_proxy_exit({"protocol": "http", "host": "", "port": 80})
)
with self.assertRaises(RequestError):
validate_proxy_exit(
parse_proxy_exit({"protocol": "http", "host": "proxy", "port": 0})
)
with self.assertRaises(RequestError):
validate_proxy_restore(
{
"binding_version": 1,
"runtime_id": "a" * 64,
"network_id": "n",
"network_exit_id": "x",
},
"bad alias",
)
self.assertFalse(valid_account_key_query({"key": ["bad key"]}, "key"))
def test_proxy_rejected_handshakes_and_docker_errors(self) -> None:
bad_http = ChunkSocket([b"HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"])
with (
patch.object(proxy_module, "_open_host", return_value=bad_http),
self.assertRaises(OSError),
):
_dial_http_proxy(ProxyExit("http", "proxy", 8080), "target:443", 1.0)
bad_socks5 = ChunkSocket([b"\x05\x02"])
with (
patch.object(proxy_module, "_open_host", return_value=bad_socks5),
self.assertRaises(OSError),
):
_dial_socks5(
ProxyExit("socks5", "proxy", 1080, "u", "p"), "127.0.0.1:80", 1.0
)
client = DockerClient("/not/a/socket")
with (
patch.object(
docker_client_module, "UnixHTTPConnection", side_effect=OSError("down")
),
self.assertRaises(DockerError),
):
client.request("GET", "/version", timeout=0.01)
with (
patch.object(
client, "request", return_value=DockerResponse(500, "bad", b"x")
),
self.assertRaises(DockerError),
):
client.expect("POST", "/x")
with (
patch.object(
client, "request", return_value=DockerResponse(404, "missing", b"")
),
self.assertRaises(FileNotFoundError),
):
client.expect("DELETE", "/gone")
def test_docker_client_transport_and_metadata_edges(self) -> None:
client = DockerClient("/unused")
response = FakeHTTPResponse(200, b"{}")
cast(Any, response).reason = "OK"
connection = FakeHTTPConnection(response)
with patch.object(
docker_client_module, "UnixHTTPConnection", return_value=connection
):
result = client.request("POST", "/version", {"ok": True})
self.assertEqual(result.status, 200)
self.assertEqual(connection.requested, [("POST", "/v1.43/version")])
huge = FakeHTTPResponse(200, b"12345")
cast(Any, huge).reason = "OK"
with (
patch.object(
docker_client_module,
"UnixHTTPConnection",
return_value=FakeHTTPConnection(huge),
),
self.assertRaises(DockerError),
):
client.request("GET", "/version", body_limit=4)
labels = {
MANAGED_LABEL: "true",
RUNTIME_ID_LABEL: "safe",
GATEWAY_MEMBER_LABEL: "true",
}
inspected = {
"Id": "a" * 64,
"Config": {"Labels": labels},
"NetworkSettings": {
"Networks": {"n": {"NetworkID": "network", "IPAddress": "198.51.100.5"}}
},
}
with patch.object(
client,
"request",
return_value=DockerResponse(200, "OK", json.dumps(inspected).encode()),
):
self.assertTrue(client.trusted_gateway_member("gateway"))
self.assertEqual(
client.container_network_address("a" * 64, "network"), "198.51.100.5"
)
bad = DockerResponse(200, "OK", b"[]")
with patch.object(client, "request", return_value=bad):
self.assertFalse(client.trusted_gateway_member("gateway"))
with self.assertRaises(DockerError):
client.managed_container_state("safe")
def test_client_connection_pull_and_inspect_edges(self) -> None:
class ConnectedSocket:
def __init__(self) -> None:
self.timeout = None
self.path = ""
def settimeout(self, value: float) -> None:
self.timeout = value
def connect(self, path: str) -> None:
self.path = path
connected = ConnectedSocket()
with patch.object(
docker_client_module.socket, "socket", return_value=connected
):
connection = docker_client_module.UnixHTTPConnection(
"/run/docker.sock", 1.0
)
connection.connect()
self.assertEqual(connected.path, "/run/docker.sock")
client = DockerClient("/unused")
with patch.object(
client,
"request",
side_effect=[
DockerResponse(404, "missing", b""),
DockerResponse(200, "OK", b"{}"),
],
) as request:
client.pull_if_missing("registry.example/repo:tag")
self.assertIn("tag=tag", request.call_args_list[1].args[1])
with (
patch.object(
client, "request", return_value=DockerResponse(500, "bad", b"no")
),
self.assertRaises(DockerError),
):
client.pull_if_missing("registry.example/repo:tag")
invalid = DockerResponse(
200,
"OK",
json.dumps(
{
"Id": "a" * 64,
"Config": {"Labels": []},
"NetworkSettings": {"Networks": {}},
}
).encode(),
)
with (
patch.object(client, "request", return_value=invalid),
self.assertRaises(DockerError),
):
client.managed_container_state("safe")
def test_cdp_error_and_proxy_auth_paths(self) -> None:
socket_ = FakeSocket([{"id": 1, "result": {"result": {"value": {"ok": True}}}}])
connection = CDPConnection(cast(websocket.WebSocket, socket_))
self.assertEqual(connection.evaluate("1"), {"ok": True})
socket_ = FakeSocket([{"id": 1, "result": {"result": {}}}])
with self.assertRaises(DouyinError):
CDPConnection(cast(websocket.WebSocket, socket_)).evaluate("1")
with self.assertRaises(DouyinError):
CDPConnection(
cast(websocket.WebSocket, FakeSocket([{"id": 1, "error": {}}]))
).command("Page.enable")
with self.assertRaises(DouyinError):
CDPConnection(
cast(websocket.WebSocket, FakeSocket([{"id": 1, "result": []}]))
).command("Page.enable")
with self.assertRaises(DouyinError):
CDPConnection(cast(websocket.WebSocket, FakeSocket([]))).wait_event(
"Page.loadEventFired", lambda _: True, timeout=0.01
)
socket_ = FakeSocket([{"id": 1, "result": {"exceptionDetails": {}}}])
with self.assertRaises(DouyinError):
CDPConnection(cast(websocket.WebSocket, socket_)).evaluate("1")
target = [
{
"type": "page",
"url": "https://www.douyin.com/1",
"webSocketDebuggerUrl": "ws://browser:9222/devtools/page/1",
}
]
http = FakeHTTPConnection(FakeHTTPResponse(200, json.dumps(target).encode()))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
patch.object(
douyin_module.websocket,
"create_connection",
side_effect=OSError("down"),
),
self.assertRaises(DouyinError),
):
DouyinBrowser(lambda _: "http://browser:9222")._connect("safe")
socks5 = ChunkSocket(
[b"\x05\x02", b"\x01\x00", b"\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x50"]
)
with patch.object(proxy_module, "_open_host", return_value=socks5):
self.assertIs(
_dial_socks5(
ProxyExit("socks5", "proxy", 1080, "u", "p"), "127.0.0.1:80", 1.0
),
socks5,
)
def test_direct_message_notice_and_uncertain_post_contract(self) -> None:
notice = normalize_notice(
{
"dm": {
"message_id": "123456",
"from_user": {"uid": "456789"},
"text": "hello",
},
"create_time": 1700000000,
}
)
self.assertIsNotNone(notice)
assert notice is not None
self.assertEqual(notice["event_type"], "dm")
self.assertEqual(notice["interactor_uid"], "456789")
self.assertEqual(notice["message_type"], "text")
self.assertEqual(notice["message_text"], "hello")
non_text = normalize_notice(
{
"dm": {
"message_id": "123457",
"from_user": {"uid": "456789"},
"message_type": "image",
"image_url": "https://example.invalid/image",
}
}
)
assert non_text is not None
self.assertEqual(non_text["message_type"], "image")
self.assertEqual(non_text["message_text"], "")
script = action_expression({"alias": "safe", "action": "follow", "target": "2"})
self.assertIn("POST_UNCERTAIN", script)
self.assertIn("BUSINESS_REJECTED", script)
def test_cdp_timeout_terminates_page_evaluation(self) -> None:
class TimeoutSocket(FakeSocket):
def __init__(self) -> None:
super().__init__([])
self.receives = 0
def recv(self) -> str:
self.receives += 1
if self.receives == 1:
raise TimeoutError("deadline")
return json.dumps({"id": 2, "result": {}})
socket_ = TimeoutSocket()
connection = CDPConnection(cast(websocket.WebSocket, socket_))
with (
patch.object(douyin_module, "CONTROL_TIMEOUT", 0.001),
self.assertRaises(DouyinError),
):
connection.command("Runtime.evaluate")
sent = [json.loads(item) for item in socket_.sent]
self.assertEqual(sent[1]["method"], "Runtime.terminateExecution")
def test_network_create_reconciliation_preserves_observed_generation(self) -> None:
client = DockerClient("/unused")
observed = TenantNetworkGeneration(name="creatorhub-safe", id="observed")
generation = TenantNetworkGeneration(name="creatorhub-safe")
with (
patch.object(
client, "inspect_tenant_network", return_value=(observed, [], True)
),
self.assertRaises(NetworkSetupError) as caught,
):
client._finish_network_create(
"creatorhub",
"safe",
"gateway",
1,
"runtime",
DockerResponse(201, "Created", b"{}"),
generation,
)
self.assertEqual(caught.exception.generation.id, "observed")
with patch.object(
client, "inspect_tenant_network", return_value=(observed, [], True)
):
result = client._finish_network_create(
"creatorhub",
"safe",
"gateway",
1,
"runtime",
DockerResponse(201, "Created", b'{"Id":"created"}'),
TenantNetworkGeneration(name="creatorhub-safe"),
)
self.assertEqual(result.id, "observed")
def test_configured_target_and_media_wait_edge_cases(self) -> None:
targets = [
{
"type": "page",
"id": "other",
"url": "https://www.douyin.com/1",
"webSocketDebuggerUrl": "ws://browser:9222/devtools/page/1",
}
]
http = FakeHTTPConnection(FakeHTTPResponse(200, json.dumps(targets).encode()))
with (
patch.object(
douyin_module.http.client, "HTTPConnection", return_value=http
),
self.assertRaisesRegex(DouyinError, "configured browser page target"),
):
DouyinBrowser(
lambda _: "http://browser:9222", target_id="missing"
)._connect("safe")
cdp = Mock()
cdp.command.return_value = {"frameId": "frame-1"}
cdp.evaluate.side_effect = [
DouyinError("page is still loading"),
{"url": "https://www.douyin.com/video/123", "readyState": "complete"},
{"error": "media_source_unavailable"},
]
browser = DouyinBrowser()
BrowserTests()._with_connection(browser, cast(BrowserCDP, cdp))
with (
patch.object(douyin_module.time, "monotonic", side_effect=[0.0, 1.0, 2.0]),
patch.object(douyin_module.time, "sleep"),
self.assertRaisesRegex(DouyinError, "media_source_unavailable"),
):
browser.get_media("safe", "https://www.douyin.com/video/123")
def test_identity_and_message_history_reject_invalid_values(self) -> None:
browser = DouyinBrowser()
for server_now in (0, float("nan")):
cast(Any, browser).get = lambda alias, target, now=server_now: (
BrowserResponse(
200,
json.dumps(
{
"status_code": 0,
"extra": {"now": now},
"user": {"uid": "123", "sec_uid": "sec"},
}
),
)
)
with (
self.subTest(server_now=server_now),
self.assertRaisesRegex(DouyinError, "platform clock"),
):
browser.identity("safe")
identity = Mock(return_value={"uid": "123"})
evaluate = Mock(return_value={"status": "succeeded", "messages": []})
cast(Any, browser).identity = identity
cast(Any, browser)._evaluate = evaluate
result = browser.message_history("safe", "123", "456", 20)
self.assertEqual(result["status"], "succeeded")
identity.assert_called_once_with("safe", "123")
self.assertIn("456", evaluate.call_args.args[1])
for expected_uid, target_uid, limit in (
("bad", "456", 20),
("123", "bad", 20),
("123", "456", 0),
("123", "456", 201),
):
with (
self.subTest(
expected_uid=expected_uid, target_uid=target_uid, limit=limit
),
self.assertRaises(DouyinError),
):
browser.message_history("safe", expected_uid, target_uid, limit)
evaluate.return_value = "bad"
with self.assertRaisesRegex(DouyinError, "message history response"):
browser.message_history("safe", "123", "456")
def test_listener_open_and_recovery_boundaries(self) -> None:
browser = Mock()
browser.identity.return_value = {
"uid": "123",
"platform_now": "2026-09-14T00:00:00+00:00",
}
connection = BrowserCDP([json.dumps({"connected": True})])
browser._connect.return_value = connection
subscription = DouyinSubscription.__new__(DouyinSubscription)
subscription.browser = browser
subscription.alias = "safe"
subscription.uid = "123"
subscription.key = "__creatorhub_notice_sub_safe"
opened, boundary_at = subscription._open_listener()
self.assertIs(opened, connection)
self.assertEqual(boundary_at, "2026-09-14T00:00:00+00:00")
constructor_connection = BrowserCDP([json.dumps({"connected": True})])
browser._connect.return_value = constructor_connection
with patch.object(threading.Thread, "start"):
constructed = DouyinSubscription(browser, "safe", "123")
self.assertEqual(constructed.queue[0]["kind"], "baseline")
constructed.stopped.set()
constructed._detail_pool.shutdown(wait=True, cancel_futures=True)
broken = BrowserCDP([json.dumps({"connected": True}), "{}"])
browser._connect.return_value = broken
browser.identity.return_value = {"uid": "123"}
with self.assertRaisesRegex(DouyinError, "event boundary"):
subscription._open_listener()
self.assertTrue(broken.closed)
old = Mock()
old.evaluate.side_effect = DouyinError("stale listener")
new = Mock()
subscription.connection = old
subscription._connection_lock = threading.RLock()
subscription._epoch = 0
subscription._initial_boundary_pending = False
subscription.stopped = threading.Event()
subscription.stopped.wait = Mock(return_value=False)
subscription._open_listener = Mock(
return_value=(new, "2026-09-14T00:00:01+00:00")
)
subscription._put = Mock()
subscription._recover()
old.close.assert_called_once_with()
self.assertIs(subscription.connection, new)
self.assertEqual(subscription._epoch, 1)
self.assertEqual(
[call.args[0]["kind"] for call in subscription._put.call_args_list],
["reconnected", "baseline"],
)
def test_external_message_history_and_generation_fences(self) -> None:
external = {
"url": "http://127.0.0.1:9222",
"target_id": "target_1",
"alias": "local-cdp",
"runtime_id": "0" * 64,
"network_id": "local-cdp",
"binding_version": 1,
}
gateway = Gateway(
cast(DockerClient, FakeDocker([])),
"creatorhub_browser",
"0123456789abcdef",
"gateway",
external_cdp=external,
)
self.assertEqual(gateway._browser_endpoint("local-cdp"), external["url"])
with self.assertRaises(GenerationConflict):
gateway._browser_endpoint("other")
with gateway._alias_lock("local-cdp"):
pass
browser = Mock()
browser.message_history.return_value = {
"status": "succeeded",
"history_source": "im_sdk_pull",
"messages": [],
}
gateway.browser = browser
generation = {
"binding_version": 1,
"runtime_id": "0" * 64,
"network_id": "local-cdp",
"expected_uid": "123",
"target_uid": "456",
"limit": 20,
}
self.assertEqual(
gateway.douyin_message_history("local-cdp", generation)["status"],
"succeeded",
)
browser.message_history.assert_called_once_with("local-cdp", "123", "456", 20)
with self.assertRaises(RequestError):
gateway.douyin_message_history(
"local-cdp", {**generation, "target_uid": "123"}
)
browser.message_history.side_effect = DouyinError("offline")
with self.assertRaises(RequestError):
gateway.douyin_message_history("local-cdp", generation)
def test_douyin_api_urls_and_external_config_edges(self) -> None:
self.assertTrue(
valid_douyin_url(
"https://www.douyin.com"
f"{gateway_module.DOUYIN_WORKS_PATH}"
"?aid=6383&device_platform=webapp&sec_user_id=sec&count=20&max_cursor=0"
)
)
self.assertTrue(
valid_douyin_url(
"https://www.douyin.com"
f"{gateway_module.DOUYIN_COMMENTS_PATH}"
"?aid=6383&device_platform=webapp&aweme_id=123&count=20&cursor=0"
)
)
self.assertFalse(
valid_douyin_url(
"https://www.douyin.com"
f"{gateway_module.DOUYIN_WORKS_PATH}"
"?aid=6383&device_platform=webapp&sec_user_id=sec&count=10&max_cursor=0"
)
)
self.assertTrue(gateway_module.valid_cdp_url("http://127.0.0.1:9222/"))
for raw in (
"https://127.0.0.1:9222",
"http://127.0.0.1:9222/devtools",
"http://127.0.0.1:9222?x=1",
"http://user:pass@127.0.0.1:9222",
"http://127.0.0.1:not-a-port",
):
with self.subTest(raw=raw):
self.assertFalse(gateway_module.valid_cdp_url(raw))
base = {
"LISTEN_ADDR": ":8081",
"DOCKER_SOCKET": "/var/run/docker.sock",
"BROWSER_NETWORK": "creatorhub_browser",
"GATEWAY_TOKEN": "0123456789abcdef",
"BROWSER_CDP_URL": "http://127.0.0.1:9222",
}
with self.assertRaises(ValueError):
load_config({**base, "BROWSER_CDP_BINDING_VERSION": "not-an-int"})
with self.assertRaises(ValueError):
load_config({**base, "BROWSER_CDP_BINDING_VERSION": "0"})
with self.assertRaises(ValueError):
load_config({**base, "BROWSER_CDP_ALIAS": "bad alias"})
class DouyinReleaseRemediationTests(unittest.TestCase):
def test_browser_action_marker_is_persisted_in_profile_storage(self) -> None:
browser = DouyinBrowser()
browser._evaluate = Mock(side_effect=[{"runtime_id": "old"}, True, True])
self.assertEqual(browser.action_ownership("safe"), {"runtime_id": "old"})
browser.set_action_ownership(
"safe", {"runtime_id": "new", "operation_id": "op-1"}
)
browser.clear_action_ownership("safe", "op-1")
self.assertEqual(browser._evaluate.call_count, 3)
self.assertIn(
"__creatorhub_action_ownership_v1",
browser._evaluate.call_args_list[1].args[1],
)
def test_gateway_action_claim_rejects_marker_from_any_runtime(self) -> None:
gateway = Gateway.__new__(Gateway)
gateway._action_ownership_lock = threading.Lock()
gateway._uncertain_actions = {}
gateway.browser = Mock()
gateway.browser.action_ownership.return_value = {"runtime_id": "old-runtime"}
with self.assertRaises(RequestError):
gateway._claim_action("safe", "new-runtime", "op-1")
gateway.browser.clear_action_ownership.assert_not_called()
gateway.browser.set_action_ownership.assert_not_called()
def test_notice_keeps_raw_gateway_receipt_time(self) -> None:
notice = normalize_notice(
{"dm": {"message_id": "1", "from_user": {"uid": "2"}, "text": "hi"}},
"2026-09-15T00:00:00+00:00",
)
assert notice is not None
self.assertEqual(notice["gateway_received_at"], "2026-09-15T00:00:00+00:00")
if __name__ == "__main__":
unittest.main()