| 1 |
"""End-to-end peer-mode: start a server in a thread, pull from the client. |
| 2 |
|
| 3 |
Avoids subprocess gymnastics — everything runs in one process. The |
| 4 |
server runs in a background thread; the test thread is the peer |
| 5 |
client. Verifies: |
| 6 |
|
| 7 |
1. The HTTP server accepts a valid token on GET /<dlm_id> |
| 8 |
2. The pack bytes arrive intact (byte-for-byte equal) |
| 9 |
3. An expired token is refused with HTTP 403 |
| 10 |
4. An unknown dlm_id is refused with HTTP 404 |
| 11 |
5. Missing token is refused with HTTP 401 |
| 12 |
""" |
| 13 |
|
| 14 |
from __future__ import annotations |
| 15 |
|
| 16 |
import threading |
| 17 |
import time |
| 18 |
import urllib.error |
| 19 |
import urllib.request |
| 20 |
from pathlib import Path |
| 21 |
|
| 22 |
import pytest |
| 23 |
|
| 24 |
from dlm.share import ServeHandle |
| 25 |
|
| 26 |
|
| 27 |
def _start_server_in_thread( |
| 28 |
tmp_path: Path, *, ttl: int = 600 |
| 29 |
) -> tuple[ServeHandle, threading.Thread, bytes]: |
| 30 |
"""Helper: pack a trivial file + start the peer server in a thread. |
| 31 |
|
| 32 |
Returns `(handle, thread, pack_bytes)`. Caller stops via |
| 33 |
`handle._server.shutdown()` + `thread.join()`. |
| 34 |
""" |
| 35 |
from dlm.share import ServeOptions, serve |
| 36 |
|
| 37 |
# Simulate a "pack" — any bytes will do for the transport test. |
| 38 |
pack = tmp_path / "fake.dlm.pack" |
| 39 |
pack_bytes = b"dlm-pack-contents-" * 256 # ~4 KB |
| 40 |
pack.write_bytes(pack_bytes) |
| 41 |
|
| 42 |
opts = ServeOptions(port=0, token_ttl_seconds=ttl) # port=0 → OS picks free port |
| 43 |
try: |
| 44 |
handle = serve("01HZTESTID", pack, opts) |
| 45 |
except PermissionError as exc: |
| 46 |
pytest.skip(f"loopback bind blocked on this host: {exc}") |
| 47 |
|
| 48 |
thread = threading.Thread(target=handle._server.serve_forever, daemon=True) |
| 49 |
thread.start() |
| 50 |
|
| 51 |
# Give the server a moment to bind. |
| 52 |
time.sleep(0.05) |
| 53 |
return handle, thread, pack_bytes |
| 54 |
|
| 55 |
|
| 56 |
def _stop_server(handle: ServeHandle, thread: threading.Thread) -> None: |
| 57 |
handle._server.shutdown() |
| 58 |
handle._server.server_close() |
| 59 |
thread.join(timeout=2.0) |
| 60 |
|
| 61 |
|
| 62 |
class TestPeerRoundTrip: |
| 63 |
def test_happy_path(self, tmp_path: Path) -> None: |
| 64 |
handle, thread, original = _start_server_in_thread(tmp_path) |
| 65 |
try: |
| 66 |
# Construct the actual bind URL from the handle — resolve_bind |
| 67 |
# returns 127.0.0.1 by default, and port 0 was replaced with |
| 68 |
# the real port by the OS on serve_forever. |
| 69 |
real_port = handle._server.server_address[1] |
| 70 |
url = f"http://127.0.0.1:{real_port}/{handle.session.dlm_id}?token={handle.token}" |
| 71 |
|
| 72 |
with urllib.request.urlopen(url, timeout=2) as resp: # noqa: S310 |
| 73 |
assert resp.status == 200 |
| 74 |
received = resp.read() |
| 75 |
assert received == original |
| 76 |
finally: |
| 77 |
_stop_server(handle, thread) |
| 78 |
|
| 79 |
def test_missing_token_refused(self, tmp_path: Path) -> None: |
| 80 |
handle, thread, _ = _start_server_in_thread(tmp_path) |
| 81 |
try: |
| 82 |
real_port = handle._server.server_address[1] |
| 83 |
url = f"http://127.0.0.1:{real_port}/{handle.session.dlm_id}" |
| 84 |
with pytest.raises(urllib.error.HTTPError) as exc_info: |
| 85 |
urllib.request.urlopen(url, timeout=2) # noqa: S310 |
| 86 |
assert exc_info.value.code == 401 |
| 87 |
finally: |
| 88 |
_stop_server(handle, thread) |
| 89 |
|
| 90 |
def test_bad_token_refused(self, tmp_path: Path) -> None: |
| 91 |
handle, thread, _ = _start_server_in_thread(tmp_path) |
| 92 |
try: |
| 93 |
real_port = handle._server.server_address[1] |
| 94 |
url = f"http://127.0.0.1:{real_port}/{handle.session.dlm_id}?token=garbage" |
| 95 |
with pytest.raises(urllib.error.HTTPError) as exc_info: |
| 96 |
urllib.request.urlopen(url, timeout=2) # noqa: S310 |
| 97 |
assert exc_info.value.code == 403 |
| 98 |
finally: |
| 99 |
_stop_server(handle, thread) |
| 100 |
|
| 101 |
def test_unknown_dlm_id_refused(self, tmp_path: Path) -> None: |
| 102 |
handle, thread, _ = _start_server_in_thread(tmp_path) |
| 103 |
try: |
| 104 |
real_port = handle._server.server_address[1] |
| 105 |
url = f"http://127.0.0.1:{real_port}/01HZDIFFERENT?token={handle.token}" |
| 106 |
with pytest.raises(urllib.error.HTTPError) as exc_info: |
| 107 |
urllib.request.urlopen(url, timeout=2) # noqa: S310 |
| 108 |
assert exc_info.value.code == 404 |
| 109 |
finally: |
| 110 |
_stop_server(handle, thread) |
| 111 |
|
| 112 |
def test_expired_token_refused(self, tmp_path: Path) -> None: |
| 113 |
# TTL of 0 → token is born expired. |
| 114 |
handle, thread, _ = _start_server_in_thread(tmp_path, ttl=0) |
| 115 |
try: |
| 116 |
time.sleep(0.01) # ensure clock moves past expiry |
| 117 |
real_port = handle._server.server_address[1] |
| 118 |
url = f"http://127.0.0.1:{real_port}/{handle.session.dlm_id}?token={handle.token}" |
| 119 |
with pytest.raises(urllib.error.HTTPError) as exc_info: |
| 120 |
urllib.request.urlopen(url, timeout=2) # noqa: S310 |
| 121 |
assert exc_info.value.code == 403 |
| 122 |
finally: |
| 123 |
_stop_server(handle, thread) |