Добавить протокол Altera Logic и общие клиенты прошивки
This commit is contained in:
57
python/tests/test_altera_logic.py
Normal file
57
python/tests/test_altera_logic.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Golden RTL vectors through the Python FFI (no Python production codec)."""
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from altera_logic import NativeAnalyzer
|
||||
|
||||
|
||||
class AnalyzerTests(unittest.TestCase):
|
||||
def test_rtl_info_and_first_configuration_vector(self):
|
||||
core = NativeAnalyzer()
|
||||
self.assertEqual(core.next_request(), bytes.fromhex("A5 01 00 00 00 A4"))
|
||||
self.assertEqual(core.next_request(), b"")
|
||||
for byte in bytes.fromhex("5A 81 00 10 00 10 32 01 E8"):
|
||||
core.feed(bytes([byte]))
|
||||
self.assertEqual(core.state, core.READY)
|
||||
with self.assertRaises(ValueError):
|
||||
core.start(49, 1, 0, 1, 1)
|
||||
core.start(49, 0, 0, 1, 1)
|
||||
self.assertEqual(core.next_request(), bytes.fromhex("A5 02 31 00 00 96"))
|
||||
core.feed(bytes.fromhex("5A 82 00 D8"))
|
||||
self.assertEqual(core.next_request(), bytes.fromhex("A5 03 00 00 00 A6"))
|
||||
|
||||
def test_error_response_is_short_even_for_info(self):
|
||||
core = NativeAnalyzer()
|
||||
core.next_request()
|
||||
core.feed(bytes.fromhex("5A 81 01 DA"))
|
||||
self.assertEqual(core.state, core.ERROR)
|
||||
self.assertEqual(core.get(1), 3)
|
||||
|
||||
def test_partial_response_times_out_without_retry(self):
|
||||
core = NativeAnalyzer()
|
||||
core.next_request()
|
||||
core.feed(bytes.fromhex("5A 81 00"))
|
||||
core.tick(1000)
|
||||
self.assertEqual(core.get(1), 4)
|
||||
self.assertEqual(core.next_request(), b"")
|
||||
|
||||
def test_demo_export_uses_capture_rate_not_current_ui_settings(self):
|
||||
core = NativeAnalyzer()
|
||||
core.reset(demo=True)
|
||||
core.start(49)
|
||||
capture = core.capture()
|
||||
self.assertEqual(len(capture.samples), 4096)
|
||||
self.assertEqual(capture.sample_rate, 1000000)
|
||||
self.assertEqual(capture.trigger_index, 2048)
|
||||
self.assertTrue(capture.demo)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/"capture.csv"
|
||||
capture.save_csv(path)
|
||||
rows = path.read_text().splitlines()
|
||||
self.assertEqual(len(rows), 4097)
|
||||
self.assertIn("time_s", rows[0])
|
||||
self.assertEqual(rows[2049].split(",")[1], "0.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
92
python/tests/test_altera_stream.py
Normal file
92
python/tests/test_altera_stream.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from altera_logic.stream import NativeStream
|
||||
|
||||
|
||||
class StreamTests(unittest.TestCase):
|
||||
def test_can_uart_identical_and_duplicates(self):
|
||||
source, can, uart = NativeStream(), NativeStream(), NativeStream()
|
||||
self.assertEqual((source.device_type, source.device_id, source.device_name), (6, 14, "Altera Logic"))
|
||||
ident, data = source.metadata_packet(7, 1000000)
|
||||
self.assertEqual(ident, 0x1EE3FE00)
|
||||
self.assertEqual(data, bytes.fromhex("01 4C 07 00 40 42 0F 00"))
|
||||
can.feed_can(ident, data)
|
||||
_, raw = source.metadata_packet(7, 1000000, uart=True)
|
||||
for byte in raw:
|
||||
uart.feed_uart(bytes([byte]))
|
||||
for i in range(100):
|
||||
ident, data = source.data_packet(7, i, i)
|
||||
can.feed_can(ident, data)
|
||||
_, raw = source.data_packet(7, i, i, uart=True)
|
||||
uart.feed_uart(raw)
|
||||
can.feed_uart(raw) # Duplicate publication through the second transport.
|
||||
self.assertEqual(can.snapshot(), uart.snapshot())
|
||||
self.assertEqual(can.stats["duplicates"], 100)
|
||||
self.assertEqual(can.stats["received"], 100)
|
||||
|
||||
def test_gap_old_session_and_counter_wrap(self):
|
||||
s = NativeStream()
|
||||
s.feed_can(*s.metadata_packet(10, 1000000))
|
||||
s.feed_can(*s.data_packet(10, 0xfffffffe, 1))
|
||||
s.feed_can(*s.data_packet(10, 1, 2))
|
||||
snap = s.snapshot()
|
||||
self.assertEqual(snap.indices, (0xfffffffe, 0x100000001))
|
||||
self.assertEqual(s.stats["missing"], 2)
|
||||
self.assertEqual(snap.breaks, (1, 1))
|
||||
s.feed_can(*s.metadata_packet(11, 2000000))
|
||||
s.feed_can(*s.data_packet(10, 5, 3))
|
||||
s.feed_can(*s.metadata_packet(10, 1000000))
|
||||
self.assertEqual(s.stats["session"], 11)
|
||||
self.assertEqual(s.snapshot().samples, ())
|
||||
s.feed_can(*s.data_packet(11, 0, 0x8001))
|
||||
self.assertEqual(s.snapshot().samples, (0x8001,))
|
||||
|
||||
def test_malformed_filter_crc_and_recovery(self):
|
||||
s = NativeStream()
|
||||
ident, meta = s.metadata_packet(1, 1000)
|
||||
s.feed_can(ident, meta, extended=False)
|
||||
s.feed_can(ident, meta, remote=True)
|
||||
s.feed_can(ident ^ (1 << 20), meta)
|
||||
self.assertFalse(s.stats["has_meta"])
|
||||
raw = bytearray(s.metadata_packet(1, 1000, uart=True)[1])
|
||||
raw[-1] ^= 1
|
||||
s.feed_uart(bytes(raw))
|
||||
self.assertEqual(s.stats["crc_errors"], 1)
|
||||
s.feed_uart(s.metadata_packet(1, 1000, uart=True)[1])
|
||||
self.assertTrue(s.stats["has_meta"])
|
||||
ident, data = s.data_packet(1, 0, 5)
|
||||
s.feed_can(ident, data[:-1])
|
||||
self.assertEqual(s.stats["invalid"], 1)
|
||||
self.assertEqual(s.stats["count"], 0)
|
||||
|
||||
def test_bounded_history_and_export(self):
|
||||
s = NativeStream()
|
||||
for _ in range(3):
|
||||
s.demo_step(4096)
|
||||
snap = s.snapshot(demo=True)
|
||||
self.assertEqual(len(snap.samples), 8192)
|
||||
self.assertEqual(snap.indices[0], 4096)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/"stream.csv"
|
||||
snap.save_csv(path)
|
||||
rows = path.read_text().splitlines()
|
||||
self.assertEqual(len(rows), 8193)
|
||||
self.assertIn("gap_before", rows[0])
|
||||
self.assertEqual(rows[1].split(",")[1], "4096")
|
||||
|
||||
def test_sparse_trace_does_not_connect_across_lost_samples(self):
|
||||
s = NativeStream()
|
||||
s.feed_can(*s.metadata_packet(1, 1000000))
|
||||
for index, value in ((0, 0), (1, 1), (4, 0)):
|
||||
s.feed_can(*s.data_packet(1, index, value))
|
||||
trace = s.snapshot().traces[0]
|
||||
self.assertEqual(trace[0], (0, 0, 1))
|
||||
self.assertIn((1, 0, 0), trace)
|
||||
self.assertIn((1, 1, 0), trace)
|
||||
self.assertIn((4, 0, 1), trace)
|
||||
self.assertNotIn((4, 1, 0), trace)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
135
python/tests/test_firmware_clients.py
Normal file
135
python/tests/test_firmware_clients.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Android wire compatibility and complete host-side flashing transactions."""
|
||||
from dataclasses import replace
|
||||
from types import SimpleNamespace
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
import pytest
|
||||
|
||||
from protocan import tms_firmware as tms
|
||||
from protocan.can_boot import CanBootTarget, CanBootTransfer
|
||||
from protocan.protocan import ProtoCanId
|
||||
from protocan.transport import build_frame
|
||||
|
||||
|
||||
def reply(command, payload=b"", tail=2):
|
||||
return tms.packet(10, command, payload) + bytes(tail)
|
||||
|
||||
|
||||
def test_android_packet_layout_and_crc():
|
||||
assert tms.crc16_modbus(b"123456789") == 0x4B37
|
||||
assert tms.init_load(10, 0xA0000, 0x1234)[:10] == bytes.fromhex("0a3a00000a0034120000")
|
||||
assert tms.load_data(10, b"\1\2\3\4")[:6] == bytes.fromhex("0a3301020304")
|
||||
assert tms.tflash(10, 0xA0000, 0x110000, 0x123456)[:14] == bytes.fromhex("0a3700000a000000110056341200")
|
||||
assert tms.extend(10, 0xA0000, 0, 2, 10, 3)[:16] == bytes.fromhex("0a3c00000a0000000000020000000a03")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command,payload,tail", [(58, b"", 2), (52, b"abcdef", 4)])
|
||||
def test_only_reserved_tail_may_be_missing(command, payload, tail):
|
||||
full = reply(command, payload, tail)
|
||||
for n in range(tail + 1):
|
||||
assert tms.normalize_reply(full[:len(full)-n], 10, command, len(full)) == full
|
||||
assert tms.normalize_reply(full[:len(full)-tail-1], 10, command, len(full)) is None
|
||||
corrupt = bytearray(full)
|
||||
corrupt[2] ^= 1
|
||||
assert tms.normalize_reply(bytes(corrupt), 10, command, len(full)) is None
|
||||
assert tms.normalize_reply(full, 11, command, len(full)) is None
|
||||
|
||||
|
||||
def run_tms(data, target, corrupt=False):
|
||||
plan = tms.programming_steps(data, target)
|
||||
response = None
|
||||
requests = []
|
||||
while True:
|
||||
try:
|
||||
request, size, timeout, title, percent = plan.send(response)
|
||||
except StopIteration:
|
||||
return requests
|
||||
requests.append(request)
|
||||
command = request[1]
|
||||
if command == 52:
|
||||
address, count = struct.unpack_from("<II", request, 2)
|
||||
offset = (address-target.flash)*2
|
||||
chunk = data[offset:offset+count].ljust(count, b"\xff")
|
||||
if corrupt:
|
||||
chunk = bytes([chunk[0] ^ 1]) + chunk[1:]
|
||||
response = reply(command, chunk, 4)
|
||||
assert len(response) == size
|
||||
elif command == 60:
|
||||
response = reply(command, struct.pack("<III", (len(data)+1)//2, 0, 0))
|
||||
else:
|
||||
response = reply(command)
|
||||
|
||||
|
||||
def test_tms_word_offsets_and_odd_final_byte_readback():
|
||||
requests = run_tms(bytes(range(256)) + b"x", tms.TmsTarget())
|
||||
assert [p[1] for p in requests] == [58, 51, 58, 51, 55, 52, 52]
|
||||
assert struct.unpack_from("<II", requests[2], 2) == (0xA0080, 1)
|
||||
assert struct.unpack_from("<II", requests[-1], 2) == (0x110080, 2)
|
||||
with pytest.raises(ValueError, match="не пройдена"):
|
||||
run_tms(b"test", tms.TmsTarget(), corrupt=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind,codes", [("spartan2e", [6, 17]), ("spartan6", [10])])
|
||||
def test_peripheral_flashing_and_verification(kind, codes):
|
||||
packets = run_tms(b"test", tms.TmsTarget(kind=kind, flash=0, board=3))
|
||||
assert [p[14] for p in packets if p[1] == 60] == codes
|
||||
assert all(p[15] == 3 for p in packets if p[1] == 60)
|
||||
|
||||
|
||||
def test_load_only_never_writes_flash():
|
||||
assert [p[1] for p in run_tms(b"test", tms.TmsTarget(load_only=True))] == [58, 51]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target,size", [(tms.TmsTarget(flash=0), 4), (tms.TmsTarget(), 0),
|
||||
(tms.TmsTarget(block_size=257), 4), (tms.TmsTarget(flash=0x17FFFF), 3),
|
||||
(tms.TmsTarget(ram=0xFFFFFFFF), 4)])
|
||||
def test_invalid_targets_fail_before_transmission(target, size):
|
||||
with pytest.raises(ValueError):
|
||||
target.validate(size)
|
||||
|
||||
|
||||
def image(data):
|
||||
return SimpleNamespace(data=data, crc32=zlib.crc32(data), version=7)
|
||||
|
||||
|
||||
def status(t, cmd, code=0, slot=1, expected=0):
|
||||
target = t.target
|
||||
can_id = ProtoCanId.build(1, 1, target.device_type, target.device, 12, target.session_id << 8 | cmd)
|
||||
return build_frame(can_id, struct.pack("<BBHI", code, slot, expected, 0), to_can=False)
|
||||
|
||||
|
||||
def enter_data(t):
|
||||
assert ProtoCanId.parse(t.start()[0].can_id).msg_type == 9
|
||||
for cmd in (2, 3, 4):
|
||||
t.handle_status(status(t, cmd))
|
||||
return t.handle_status(status(t, 5))[0]
|
||||
|
||||
|
||||
def test_legacy_can_full_transaction_and_retransmission():
|
||||
t = CanBootTransfer(image(bytes(range(130))), CanBootTarget())
|
||||
frames = enter_data(t)
|
||||
assert len(frames) == 16
|
||||
assert all(ProtoCanId.parse(f.can_id).msg_type == 11 for f in frames)
|
||||
frames, _ = t.handle_status(status(t, 0, code=8, expected=8))
|
||||
assert ProtoCanId.parse(frames[0].can_id).body == 8
|
||||
assert frames[-1].data == bytes([128,129]) + b"\xff" * 6
|
||||
frames, _ = t.handle_status(status(t, 0, expected=17))
|
||||
assert t.stage == "verify"
|
||||
for cmd in (6, 7, 9):
|
||||
t.handle_status(status(t, cmd))
|
||||
assert t.finished and t.percent == 100
|
||||
|
||||
|
||||
def test_can_rejects_unsent_blocks_wrong_slot_and_foreign_session():
|
||||
t = CanBootTransfer(image(bytes(200)), CanBootTarget())
|
||||
enter_data(t)
|
||||
with pytest.raises(RuntimeError, match="непереданный"):
|
||||
t.handle_status(status(t, 0, expected=20))
|
||||
foreign = CanBootTransfer(t.image, replace(t.target, session_id=2))
|
||||
assert not t.accepts(status(foreign, 0))
|
||||
t = CanBootTransfer(image(bytes(8)), CanBootTarget())
|
||||
t.handle_status(status(t, 2))
|
||||
t.handle_status(status(t, 3))
|
||||
with pytest.raises(RuntimeError, match="слот"):
|
||||
t.handle_status(status(t, 4, slot=2))
|
||||
192
python/tests/test_firmware_database.py
Normal file
192
python/tests/test_firmware_database.py
Normal file
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from setprotocol.firmware_catalog import FirmwareRelease
|
||||
from setprotocol.firmware_database import (
|
||||
Credentials, FirmwareDatabase, GiteaFirmwarePublisher, GiteaRepository, HttpsClient,
|
||||
)
|
||||
from setprotocol.firmware_publish import FirmwarePublication
|
||||
|
||||
|
||||
class Response(io.BytesIO):
|
||||
def __init__(self, data, headers=None):
|
||||
super().__init__(data)
|
||||
self.headers = headers or {}
|
||||
|
||||
|
||||
class Server:
|
||||
"""In-memory HTTP boundary; no real credentials, network or repository writes."""
|
||||
def __init__(self, *, bad_image=False, conflict=False, missing_release=False):
|
||||
self.manifest = {"windows": {"versionCode": 42}, "firmware": {"releases": []}}
|
||||
self.assets = {}
|
||||
self.calls = []
|
||||
self.bad_image = bad_image
|
||||
self.conflict = conflict
|
||||
self.missing_release = missing_release
|
||||
|
||||
def open(self, url, *, method="GET", data=None, **kwargs):
|
||||
self.calls.append((method, url))
|
||||
if self.missing_release and "/releases/tags/" in url:
|
||||
raise HTTPError(url, 404, "Not found", {}, None)
|
||||
if "/releases/download/" in url:
|
||||
return Response(b"wrong" if self.bad_image else self.assets[url.rsplit("/", 1)[1]])
|
||||
if "/raw/branch/" in url:
|
||||
result = self.manifest
|
||||
elif "/contents/" in url:
|
||||
if method == "PUT":
|
||||
if self.conflict:
|
||||
raise HTTPError(url, 409, "Conflict", {}, None)
|
||||
payload = json.loads(data)
|
||||
assert payload["sha"] == "revision-1"
|
||||
self.manifest = json.loads(base64.b64decode(payload["content"]))
|
||||
result = {"sha": "revision-1", "content": base64.b64encode(json.dumps(self.manifest).encode()).decode()}
|
||||
elif "/assets" in url:
|
||||
if method == "POST":
|
||||
self.assets[url.split("?name=", 1)[1]] = data
|
||||
result = {"id": 2}
|
||||
else:
|
||||
result = [{"name": name, "id": index} for index, name in enumerate(self.assets)]
|
||||
else:
|
||||
result = {"id": 1}
|
||||
return Response(json.dumps(result).encode())
|
||||
|
||||
|
||||
class FirmwareDatabaseTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name)
|
||||
self.image = self.root / "image.bin"
|
||||
self.image.write_bytes(b"test firmware")
|
||||
self.repository = GiteaRepository("https://git.example", "team", "images")
|
||||
self.publication = FirmwarePublication(self.image, "DEVICE", "1.2.3", 0x10203, "can")
|
||||
|
||||
def test_publish_read_and_download_round_trip_and_idempotency(self):
|
||||
server = Server()
|
||||
publisher = GiteaFirmwarePublisher(self.repository, client=server)
|
||||
entry = publisher.publish(self.publication)
|
||||
self.assertEqual(server.manifest["windows"], {"versionCode": 42})
|
||||
self.assertEqual(server.manifest["firmware"]["catalogVersion"], 1)
|
||||
first_commit = next(i for i, (method, _) in enumerate(server.calls) if method == "PUT")
|
||||
first_verification = next(i for i, (_, url) in enumerate(server.calls) if "/releases/download/" in url)
|
||||
self.assertLess(first_verification, first_commit)
|
||||
publisher.publish(self.publication)
|
||||
self.assertEqual(sum(method == "PUT" for method, _ in server.calls), 1)
|
||||
self.assertEqual(len(server.assets), 1)
|
||||
database = FirmwareDatabase(self.repository.manifest_url, self.root / "cache", client=server)
|
||||
rows = database.read_catalog(product="device", transport="can")
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(database.read_catalog(transport="rs485"), [])
|
||||
progress = []
|
||||
downloaded = database.download(rows[0], progress.append)
|
||||
self.assertEqual(downloaded.read_bytes(), self.image.read_bytes())
|
||||
self.assertEqual(progress[-1], 100)
|
||||
count = len(server.calls)
|
||||
self.assertEqual(database.download(rows[0]), downloaded)
|
||||
self.assertEqual(len(server.calls), count)
|
||||
self.assertEqual(entry["fileName"], "image.bin")
|
||||
|
||||
def test_failed_image_verification_never_writes_catalog(self):
|
||||
server = Server(bad_image=True)
|
||||
with self.assertRaisesRegex(ValueError, "SHA-256"):
|
||||
GiteaFirmwarePublisher(self.repository, client=server).publish(self.publication)
|
||||
self.assertFalse(any(method == "PUT" for method, _ in server.calls))
|
||||
|
||||
def test_rebuilt_image_keeps_old_asset_and_manifest_on_conflict(self):
|
||||
server = Server()
|
||||
publisher = GiteaFirmwarePublisher(self.repository, client=server)
|
||||
publisher.publish(self.publication)
|
||||
before = json.dumps(server.manifest)
|
||||
self.image.write_bytes(b"new firmware")
|
||||
server.conflict = True
|
||||
with self.assertRaises(HTTPError) as caught:
|
||||
publisher.publish(self.publication)
|
||||
caught.exception.close()
|
||||
self.assertEqual(json.dumps(server.manifest), before)
|
||||
self.assertEqual(len(server.assets), 2)
|
||||
self.assertFalse(any(method == "DELETE" for method, _ in server.calls))
|
||||
|
||||
def test_preflight_is_offline(self):
|
||||
server = Server()
|
||||
GiteaFirmwarePublisher(self.repository, client=server).preflight(self.publication)
|
||||
self.assertEqual(server.calls, [])
|
||||
|
||||
def test_missing_release_is_created(self):
|
||||
server = Server(missing_release=True)
|
||||
GiteaFirmwarePublisher(self.repository, client=server).publish(self.publication)
|
||||
self.assertTrue(any(method == "POST" and url.endswith("/releases")
|
||||
for method, url in server.calls))
|
||||
|
||||
def test_download_size_limit_and_catalog_readback_failure(self):
|
||||
from setprotocol.firmware_publish import MAX_FIRMWARE_BYTES
|
||||
client = Mock()
|
||||
client.open.return_value = Response(b"", {"Content-Length": str(MAX_FIRMWARE_BYTES + 1)})
|
||||
database = FirmwareDatabase(self.repository.manifest_url, self.root / "cache", client=client)
|
||||
release = FirmwareRelease("D", "1", 1, "https://git.example/a.bin", "ab" * 32, "a.bin")
|
||||
with self.assertRaisesRegex(ValueError, "maximum size"):
|
||||
database.download(release)
|
||||
self.assertEqual(list((self.root / "cache").rglob("*.part")), [])
|
||||
|
||||
server = Server()
|
||||
original_open = server.open
|
||||
|
||||
def ignore_manifest_write(url, **kwargs):
|
||||
if kwargs.get("method") == "PUT":
|
||||
return Response(b"{}")
|
||||
return original_open(url, **kwargs)
|
||||
|
||||
server.open = ignore_manifest_write
|
||||
with self.assertRaisesRegex(RuntimeError, "readback"):
|
||||
GiteaFirmwarePublisher(self.repository, client=server).publish(self.publication)
|
||||
|
||||
def test_bad_download_cleans_staging_and_rejects_unsafe_filename(self):
|
||||
release = FirmwareRelease("D", "1", 1, "https://git.example/a.bin",
|
||||
hashlib.sha256(b"good").hexdigest(), "a.bin")
|
||||
client = Mock()
|
||||
client.open.return_value = Response(b"bad")
|
||||
database = FirmwareDatabase(self.repository.manifest_url, self.root / "cache", client=client)
|
||||
with self.assertRaisesRegex(ValueError, "SHA-256"):
|
||||
database.download(release)
|
||||
self.assertEqual(list((self.root / "cache").rglob("*.part")), [])
|
||||
with self.assertRaises(ValueError):
|
||||
database.download(replace(release, file_name="../escaped.bin"))
|
||||
self.assertEqual(client.open.call_count, 1)
|
||||
|
||||
def test_https_credentials_do_not_follow_cross_origin_redirect(self):
|
||||
client = HttpsClient("https://git.example", Credentials("test", "secret"))
|
||||
client.opener = Mock()
|
||||
client.opener.open.side_effect = [
|
||||
HTTPError("https://git.example/a", 302, "Redirect", {"Location": "https://cdn.example/a"}, None),
|
||||
Response(b"image"),
|
||||
]
|
||||
client.open("https://git.example/a").close()
|
||||
first, second = [call.args[0] for call in client.opener.open.call_args_list]
|
||||
self.assertIn("Authorization", first.headers)
|
||||
self.assertNotIn("Authorization", second.headers)
|
||||
|
||||
def test_http_redirect_and_write_redirect_are_rejected(self):
|
||||
for method, destination in (("GET", "http://git.example/a"), ("POST", "https://cdn.example/a")):
|
||||
with self.subTest(method=method):
|
||||
client = HttpsClient("https://git.example")
|
||||
client.opener = Mock()
|
||||
client.opener.open.side_effect = HTTPError(
|
||||
"https://git.example/a", 302, "Redirect", {"Location": destination}, None)
|
||||
with self.assertRaises((ValueError, HTTPError)) as caught:
|
||||
client.open("https://git.example/a", method=method)
|
||||
if isinstance(caught.exception, HTTPError):
|
||||
caught.exception.close()
|
||||
self.assertEqual(client.opener.open.call_count, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user