69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
|
|
"""Gopher listener: the RFC 4266 root-menu compatibility fix, dot-stuffing."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import socket
|
||
|
|
import threading
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from smolweb.servers.gopher import GopherServer
|
||
|
|
from smolweb.site import Site
|
||
|
|
|
||
|
|
|
||
|
|
def _free_port() -> int:
|
||
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
||
|
|
probe.bind(("127.0.0.1", 0))
|
||
|
|
return probe.getsockname()[1]
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def gopher_server(site: Site):
|
||
|
|
port = _free_port()
|
||
|
|
server = GopherServer(("127.0.0.1", port), site, "localhost")
|
||
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||
|
|
thread.start()
|
||
|
|
yield server
|
||
|
|
server.shutdown()
|
||
|
|
server.server_close()
|
||
|
|
|
||
|
|
|
||
|
|
def _request(server: GopherServer, selector: str) -> bytes:
|
||
|
|
with socket.create_connection(("127.0.0.1", server.port), timeout=5) as sock:
|
||
|
|
sock.sendall(f"{selector}\r\n".encode("utf-8"))
|
||
|
|
chunks = []
|
||
|
|
while True:
|
||
|
|
chunk = sock.recv(65536)
|
||
|
|
if not chunk:
|
||
|
|
break
|
||
|
|
chunks.append(chunk)
|
||
|
|
return b"".join(chunks)
|
||
|
|
|
||
|
|
|
||
|
|
class TestGopherRootMenu:
|
||
|
|
def test_empty_selector_returns_a_one_item_menu(self, gopher_server: GopherServer):
|
||
|
|
# A bare gopher://host/ has no item-type prefix, which RFC 4266
|
||
|
|
# defines as defaulting to type '1' (menu) -- real clients parse
|
||
|
|
# the response as tab-delimited menu lines, not prose, so the
|
||
|
|
# homepage's own rendered text must never be sent back here.
|
||
|
|
response = _request(gopher_server, "")
|
||
|
|
assert response == f"0Home\t/\tlocalhost\t{gopher_server.port}\r\n.\r\n".encode("utf-8")
|
||
|
|
|
||
|
|
def test_menu_link_selector_resolves_to_real_content(self, gopher_server: GopherServer):
|
||
|
|
# The menu's own selector ("/") must not trigger the same
|
||
|
|
# menu-synthesis branch -- only the truly empty selector does.
|
||
|
|
response = _request(gopher_server, "/")
|
||
|
|
assert response.endswith(b".\r\n")
|
||
|
|
assert b"Hello." in response
|
||
|
|
assert not response.startswith(b"0Home\t")
|
||
|
|
|
||
|
|
def test_non_root_selector_is_unaffected(self, gopher_server: GopherServer):
|
||
|
|
response = _request(gopher_server, "about")
|
||
|
|
assert b"Body." in response
|
||
|
|
assert not response.startswith(b"0Home\t")
|
||
|
|
|
||
|
|
|
||
|
|
class TestGopherDotStuffing:
|
||
|
|
def test_not_found_response_is_well_formed(self, gopher_server: GopherServer):
|
||
|
|
response = _request(gopher_server, "nope")
|
||
|
|
assert response == b"Not found.\n.\r\n"
|