"""Transport regression cases observed while downloading the full FNS snapshot.""" from contextlib import contextmanager from io import BytesIO from types import SimpleNamespace from unittest.mock import patch import pytest import requests from apps.parsers.registry_http import download_registry_archive from apps.parsers.registry_snapshots import SnapshotValidationError URL = "https://file.nalog.ru/archive.zip" HOSTS = frozenset({"file.nalog.ru"}) def _response( chunks, *, status=206, content_range="bytes 0-5/6", etag='"v1"', length=None ): headers = {} if content_range is not None: headers["Content-Range"] = content_range if etag is not None: headers["ETag"] = etag if length is not None: headers["Content-Length"] = str(length) def content(**_): for chunk in chunks: if isinstance(chunk, Exception): raise chunk yield chunk return SimpleNamespace(status_code=status, headers=headers, iter_content=content) def _download(responses, **kwargs): calls = [] responses = iter(responses) @contextmanager def response(_session, url, *, hosts, headers): assert url == URL assert hosts == HOSTS calls.append(headers) yield next(responses) handle = BytesIO() with patch("apps.parsers.registry_http.registry_response", side_effect=response): result = download_registry_archive( object(), URL, handle, hosts=HOSTS, maximum=1024, range_bytes=6, **kwargs ) return handle.getvalue(), result, calls def test_interrupted_body_resumes_only_missing_bytes_with_validator(): content, count, calls = _download( [ _response([b"ab", requests.exceptions.ChunkedEncodingError("interrupted")]), _response([b"cdef"], content_range="bytes 2-5/6"), ] ) assert content == b"abcdef" assert count == 6 assert calls[1]["Range"] == "bytes=2-7" assert calls[1]["If-Range"] == '"v1"' @pytest.mark.parametrize( ("next_response", "reason"), [ ( _response([b"cdef"], content_range="bytes 2-5/6", etag='"v2"'), "source_changed_during_download", ), ( _response([b"cdefgh"], content_range="bytes 2-7/8"), "source_changed_during_download", ), (_response([b"abcdef"], status=200, length=6), "source_range_not_honored"), ( _response([b"cdef"], content_range="bytes 0-3/6"), "unexpected_source_content_range", ), ], ) def test_resume_rejects_changed_or_unproven_entity(next_response, reason): with pytest.raises(SnapshotValidationError, match=reason): _download( [ _response( [b"ab", requests.exceptions.ChunkedEncodingError("interrupted")] ), next_response, ] ) def test_no_range_server_restarts_entire_body_after_interruption(): content, count, calls = _download( [ _response( [b"ab", requests.exceptions.ChunkedEncodingError("interrupted")], status=200, length=6, ), _response([b"abcdef"], status=200, length=6), ] ) assert content == b"abcdef" assert count == 6 assert calls[1]["Range"] == "bytes=0-5" assert "If-Range" not in calls[1] def test_range_without_validator_is_rejected(): with pytest.raises( SnapshotValidationError, match="source_resume_validator_missing" ): _download([_response([b"abcdef"], etag=None)]) def test_last_modified_is_used_when_strong_etag_is_unavailable(): first = _response( [b"ab", requests.exceptions.ChunkedEncodingError("interrupted")], etag='W/"weak"', ) second = _response([b"cdef"], content_range="bytes 2-5/6", etag='W/"weak"') for response in (first, second): response.headers["Last-Modified"] = "Sat, 15 Aug 2026 09:46:30 GMT" content, _, calls = _download([first, second]) assert content == b"abcdef" assert calls[1]["If-Range"] == "Sat, 15 Aug 2026 09:46:30 GMT" def test_body_retry_count_is_bounded(): with pytest.raises(requests.exceptions.ChunkedEncodingError): _download( [ _response([requests.exceptions.ChunkedEncodingError("interrupted")]) for _ in range(2) ], max_retries=1, ) def test_oversized_archive_is_rejected_before_reading_body(): with pytest.raises(SnapshotValidationError, match="source_response_too_large"): _download([_response([], content_range="bytes 0-5/2048")])