Skip to content

Commit 0067f5d

Browse files
wip 1
1 parent 79739c1 commit 0067f5d

11 files changed

Lines changed: 97 additions & 43 deletions

webshotd/config/static_config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ components_manager:
4444
.yml: application/yaml
4545
__default__: text/plain
4646
config:
47-
url_bytes_max: 32768
47+
url_bytes_max: $url_bytes_max
48+
url_bytes_max#fallback: 32768
4849
allowlist_only: $allowlist_only
4950
https_only: $https_only
5051
state_dir: $state_dir

webshotd/src/browser_probe_handler.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ void CleanupProbeSession(
349349
[[nodiscard]] Expected<dto::BrowserProbeResponse, String>
350350
RunProbe(const dto::BrowserProbeRequest &request, const ProbeConfig &config, eng::Deadline deadline)
351351
{
352+
// Fail fast before any browser/CDP work: the request timeout can be very small in tests.
353+
if (usize{request.url.size()} > config.svc_config.UrlBytesMax())
354+
return Unex{"url too long"_t};
355+
352356
auto browser = TRY(
353357
crawler::BrowserSession::Make(
354358
config.dns_resolver_, config.process_starter_, config.fs_task_processor_,
@@ -398,8 +402,6 @@ RunProbe(const dto::BrowserProbeRequest &request, const ProbeConfig &config, eng
398402

399403
browser->MarkPhase("navigate");
400404
dto::PageNavigateParams navigate_params;
401-
if (usize{request.url.size()} > config.svc_config.UrlBytesMax())
402-
return Unex{"url too long"_t};
403405
navigate_params.url = request.url;
404406
const auto navigate_result = TRY_MAP_ERR(
405407
cdp_session.Send<dto::PageNavigateResult>("Page.navigate"_t, navigate_params),

webshotd/test/allowlist_handler.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import pytest
2+
from helper.config_hooks import enable_allowlist_only
23
from helper.constants import TEST_ASSET_HOST, TEST_HOST
34

45

5-
def _enable_allowlist_only(_config_yaml, config_vars):
6-
config_vars["allowlist_only"] = True
7-
8-
96
@pytest.mark.asyncio
107
async def test_allowlist_check_not_exposed_on_main_listener(service_client):
118
resp = await service_client.post("/v1/allowlist/check", json={"link": "http://example.com/"})
@@ -87,7 +84,7 @@ async def test_regular_mode_allowlist_overrides_denylist(service_client, monitor
8784
assert resp.status == 202
8885

8986

90-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_allowlist_only])
87+
@pytest.mark.uservice_oneshot(config_hooks=[enable_allowlist_only])
9188
@pytest.mark.asyncio
9289
async def test_allowlist_only_blocks_non_allowlisted_seed(service_client):
9390
resp = await service_client.post(
@@ -98,7 +95,7 @@ async def test_allowlist_only_blocks_non_allowlisted_seed(service_client):
9895
assert resp.json()["error"]["message"] == "link not in allowlist"
9996

10097

101-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_allowlist_only])
98+
@pytest.mark.uservice_oneshot(config_hooks=[enable_allowlist_only])
10299
@pytest.mark.asyncio
103100
async def test_allowlist_only_denylist_wins(service_client, monitor_client):
104101
link = f"https://{TEST_HOST}/allowlist-only-denylist-wins"

webshotd/test/capture_flow.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
_wacz_entries,
1515
_wacz_zip_compress_type,
1616
)
17+
from helper.config_hooks import set_url_bytes_max
1718
from helper.constants import TEST_HOST
1819
from helper.prefix import prefix_key_from_link
1920
from helper.waiters import wait_for_job_status, wait_for_purge
2021

2122
_CDP_URL_BYTES_MAX_SMALL = 64
2223

2324

24-
def _patch_url_bytes_max_small(config_yaml, _config_vars):
25-
components = config_yaml["components_manager"]["components"]
26-
components["config"]["url_bytes_max"] = _CDP_URL_BYTES_MAX_SMALL
25+
def _patch_url_bytes_max_small(config_yaml, config_vars):
26+
set_url_bytes_max(config_yaml, config_vars, _CDP_URL_BYTES_MAX_SMALL)
2727

2828

2929
@pytest.mark.asyncio
@@ -277,7 +277,8 @@ async def test_capture_records_main_document_redirect_in_wacz(
277277

278278
@pytest.mark.uservice_oneshot(config_hooks=[_patch_url_bytes_max_small])
279279
@pytest.mark.asyncio
280-
async def test_cdp_ingest_caps_url_bytes_max(monitor_client):
280+
async def test_cdp_ingest_caps_url_bytes_max(service_client, monitor_client):
281+
del service_client
281282
base = "http://test-target/?"
282283
over = base + ("a" * (_CDP_URL_BYTES_MAX_SMALL - len(base) + 1))
283284

webshotd/test/capture_flow_https.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22
from helper.capture_flow import (
33
_assert_missing_job_fields,
44
_capture_and_wait,
5-
_enable_allowlist_only,
6-
_enable_https_only,
75
_probe_replay,
86
_wacz_archive_text,
97
_wacz_cdx_statuses_for_root_url,
108
_wacz_cdx_statuses_for_url,
119
_wacz_entries,
1210
)
11+
from helper.config_hooks import enable_allowlist_only, enable_https_only
1312
from helper.constants import TEST_HOST, UNTRUSTED_TEST_HOST
1413
from helper.waiters import wait_for_job_status
1514

1615

17-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_allowlist_only])
16+
@pytest.mark.uservice_oneshot(config_hooks=[enable_allowlist_only])
1817
@pytest.mark.asyncio
1918
async def test_allowlist_only_blocks_non_allowlisted_redirect_target(
2019
service_client, monitor_client
@@ -55,7 +54,7 @@ async def test_https_first_succeeds_when_http_fails(
5554
assert not _wacz_cdx_statuses_for_url(wacz, f"http://{TEST_HOST}/https-first-http-fails")
5655

5756

58-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_https_only])
57+
@pytest.mark.uservice_oneshot(config_hooks=[enable_https_only])
5958
@pytest.mark.asyncio
6059
async def test_https_only_accepts_http_seed_and_crawls_https(
6160
service_client, browser_probe, download_wacz, service_baseurl
@@ -94,7 +93,7 @@ async def test_https_first_falls_back_to_http_when_https_no_response(
9493
assert not _wacz_cdx_statuses_for_url(wacz, f"https://{TEST_HOST}/http-fallback-success")
9594

9695

97-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_https_only])
96+
@pytest.mark.uservice_oneshot(config_hooks=[enable_https_only])
9897
@pytest.mark.asyncio
9998
async def test_https_only_does_not_fall_back_to_http(service_client):
10099
link = f"http://{TEST_HOST}/http-fallback-success"

webshotd/test/capture_flow_subresources.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import pytest
22
from helper.capture_flow import (
33
_capture_and_wait,
4-
_enable_allowlist_only,
5-
_enable_https_only,
64
_probe_replay,
75
_wacz_archive_text,
86
_wacz_cdx_statuses_for_url,
97
)
8+
from helper.config_hooks import enable_allowlist_only, enable_https_only
109
from helper.constants import TEST_ASSET_HOST, TEST_HOST
1110
from helper.waiters import wait_for_job_status
1211

@@ -101,7 +100,7 @@ async def test_regular_mode_allowlist_overrides_denylisted_subresource_fetch(
101100
assert any("denylist" in entry for entry in replay["console"])
102101

103102

104-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_allowlist_only])
103+
@pytest.mark.uservice_oneshot(config_hooks=[enable_allowlist_only])
105104
@pytest.mark.asyncio
106105
async def test_allowlist_only_fetches_allowlisted_subresources(
107106
service_client, monitor_client, browser_probe, download_wacz, service_baseurl
@@ -123,7 +122,7 @@ async def test_allowlist_only_fetches_allowlisted_subresources(
123122
assert 200 in _wacz_cdx_statuses_for_url(wacz, script)
124123

125124

126-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_allowlist_only])
125+
@pytest.mark.uservice_oneshot(config_hooks=[enable_allowlist_only])
127126
@pytest.mark.asyncio
128127
async def test_allowlist_only_blocks_non_allowlisted_subresources(
129128
service_client, monitor_client, browser_probe, download_wacz, service_baseurl
@@ -159,7 +158,7 @@ async def test_capture_fetches_https_subresource_assets(
159158
assert any("asset" in entry for entry in replay["console"])
160159

161160

162-
@pytest.mark.uservice_oneshot(config_hooks=[_enable_https_only])
161+
@pytest.mark.uservice_oneshot(config_hooks=[enable_https_only])
163162
@pytest.mark.asyncio
164163
async def test_https_only_blocks_http_subresource_fetch(
165164
service_client, browser_probe, download_wacz, service_baseurl

webshotd/test/conftest.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import copy
23
import pathlib
34
import shutil
45
import tempfile
@@ -214,7 +215,7 @@ def userver_pg_config(pgsql_local, pg_gate):
214215
db_uri = {name: conn.get_uri() for name, conn in pgsql_local.items()}
215216
gate_host, gate_port = pg_gate.get_sockname_for_clients()
216217

217-
def _patch(config_yaml, _config_vars):
218+
def _patch(config_yaml, config_vars):
218219
components = config_yaml["components_manager"]["components"]
219220
for component_name in ("capture_meta_db", "shared_state_db"):
220221
conninfo = db_uri[component_name]
@@ -226,6 +227,9 @@ def _patch(config_yaml, _config_vars):
226227
else:
227228
netloc = f"{gate_host}:{gate_port}"
228229
updated = parsed._replace(netloc=netloc)
230+
# Prefer config-vars for templated fields, but also patch the resolved YAML so
231+
# the hook remains effective even if config-vars resolution happens earlier.
232+
config_vars[f"pg_{component_name}_dsn"] = urlunparse(updated)
229233
component = components[component_name]
230234
component["dbconnection"] = urlunparse(updated)
231235
component.pop("dbalias", None)
@@ -261,7 +265,8 @@ def allowed_url_prefixes_extra(s3_gate_port):
261265
def patch_s3_config(s3_gate_port):
262266
del s3_gate_port
263267

264-
def _patch(config_yaml, _config_vars):
268+
def _patch(config_yaml, config_vars):
269+
del config_vars
265270
components = config_yaml["components_manager"]["components"]
266271
cfg = components["config"]
267272
cfg["s3_timeout_ms"] = _TESTSUITE_S3_TIMEOUT_MS
@@ -275,16 +280,22 @@ def _patch(config_yaml, _config_vars):
275280

276281
@pytest.fixture(scope="session")
277282
def service_config_path_temp(
283+
daemon_scoped_mark,
278284
service_tmpdir,
279-
_service_config_hooked,
285+
service_config_yaml,
286+
service_config_vars,
280287
service_binary: pathlib.Path,
281288
service_source_dir: pathlib.Path,
282289
testsuite_webshotd_state_dir: pathlib.Path,
283290
s3_bucket_name: str,
284291
) -> pathlib.Path:
292+
# Mark this fixture as per-daemon. For oneshot tests, daemon_scoped_mark changes
293+
# and pytest tears down/recreates the whole dependency tree.
294+
del daemon_scoped_mark
295+
285296
dst_path = service_tmpdir / "config.yaml"
286-
config_yaml = dict(_service_config_hooked.config_yaml)
287-
config_vars = dict(_service_config_hooked.config_vars)
297+
config_yaml = copy.deepcopy(service_config_yaml)
298+
config_vars = copy.deepcopy(service_config_vars)
288299
runtime_layout = runtime_layout_from_binary(service_binary)
289300
config_vars["rapidoc_assets_dir"] = str(runtime_layout.rapidoc_assets_dir)
290301
config_vars["openapi_public_dir"] = str(service_source_dir.parent / "schema" / "public")
@@ -295,6 +306,9 @@ def service_config_path_temp(
295306
config_vars["state_dir"] = str(testsuite_webshotd_state_dir)
296307
config_vars["s3_bucket"] = s3_bucket_name
297308
config_vars["public_base_url"] = f"http://127.0.0.1:8333/{s3_bucket_name}"
309+
# Ensure url_bytes_max always exists in config_vars so oneshot config hooks may override it.
310+
# static_config.yaml uses $url_bytes_max with a fallback; config vars must be concrete.
311+
config_vars.setdefault("url_bytes_max", 32768)
298312
config_vars.update(task_processor_config_vars(config_yaml))
299313

300314
components = config_yaml["components_manager"]["components"]

webshotd/test/deduplication.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from helper.waiters import wait_for_job_status
66

77

8-
def _patch_dedup_crawler_timeouts(_config_yaml, config_vars):
8+
def _patch_dedup_crawler_timeouts(config_yaml, config_vars):
99
# Dedup performs two full crawl jobs in one test; give only this test a bit more crawl headroom.
10+
del config_yaml
1011
config_vars["crawler_run_timeout_sec"] = 14
1112
config_vars["crawler_job_overhead_timeout_sec"] = 8
1213
config_vars["crawler_devtools_startup_timeout_sec"] = 10

webshotd/test/helper/capture_flow.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
from zipfile import ZipFile
77

88
import pytest
9+
from helper.config_hooks import enable_allowlist_only, enable_https_only
910
from helper.waiters import wait_for_job_status
1011
from minio import Minio
1112

13+
_enable_allowlist_only = enable_allowlist_only
1214

13-
def _enable_allowlist_only(_config_yaml, config_vars):
14-
config_vars["allowlist_only"] = True
1515

16-
17-
def _enable_https_only(_config_yaml, config_vars):
18-
config_vars["https_only"] = True
16+
_enable_https_only = enable_https_only
1917

2018

2119
def _assert_missing_job_fields(job: dict, *names: str) -> None:
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
def _require_components(config_yaml) -> dict:
2+
try:
3+
components = config_yaml["components_manager"]["components"]
4+
except KeyError as exc:
5+
raise RuntimeError("config_yaml missing components_manager.components") from exc
6+
if not isinstance(components, dict):
7+
raise RuntimeError("config_yaml components_manager.components must be a dict")
8+
return components
9+
10+
11+
def set_component_field(config_yaml, component: str, field: str, value) -> None:
12+
components = _require_components(config_yaml)
13+
cfg = components.get(component)
14+
if not isinstance(cfg, dict):
15+
raise RuntimeError(f"component {component!r} config must be a dict")
16+
cfg[field] = value
17+
18+
19+
def set_config_field(config_yaml, field: str, value) -> None:
20+
set_component_field(config_yaml, "config", field, value)
21+
22+
23+
def set_config_var(config_vars, name: str, value) -> None:
24+
config_vars[name] = value
25+
26+
27+
def set_config_var_and_field(config_yaml, config_vars, var_name: str, field: str, value) -> None:
28+
# Prefer config_vars, but also patch config_yaml for robustness: some testsuite paths
29+
# may write config_vars once per session, while per-test oneshot hooks still need to apply.
30+
set_config_var(config_vars, var_name, value)
31+
set_config_field(config_yaml, field, value)
32+
33+
34+
def enable_allowlist_only(config_yaml, config_vars) -> None:
35+
set_config_var_and_field(config_yaml, config_vars, "allowlist_only", "allowlist_only", True)
36+
37+
38+
def enable_https_only(config_yaml, config_vars) -> None:
39+
set_config_var_and_field(config_yaml, config_vars, "https_only", "https_only", True)
40+
41+
42+
def set_url_bytes_max(config_yaml, config_vars, value: int) -> None:
43+
set_config_var_and_field(config_yaml, config_vars, "url_bytes_max", "url_bytes_max", value)

0 commit comments

Comments
 (0)