You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
8.7 KiB
271 lines
8.7 KiB
import pytest |
|
|
|
from .._events import ( |
|
ConnectionClosed, |
|
Data, |
|
EndOfMessage, |
|
Event, |
|
InformationalResponse, |
|
Request, |
|
Response, |
|
) |
|
from .._state import ( |
|
_SWITCH_CONNECT, |
|
_SWITCH_UPGRADE, |
|
CLIENT, |
|
CLOSED, |
|
ConnectionState, |
|
DONE, |
|
IDLE, |
|
MIGHT_SWITCH_PROTOCOL, |
|
MUST_CLOSE, |
|
SEND_BODY, |
|
SEND_RESPONSE, |
|
SERVER, |
|
SWITCHED_PROTOCOL, |
|
) |
|
from .._util import LocalProtocolError |
|
|
|
|
|
def test_ConnectionState() -> None: |
|
cs = ConnectionState() |
|
|
|
# Basic event-triggered transitions |
|
|
|
assert cs.states == {CLIENT: IDLE, SERVER: IDLE} |
|
|
|
cs.process_event(CLIENT, Request) |
|
# The SERVER-Request special case: |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
# Illegal transitions raise an error and nothing happens |
|
with pytest.raises(LocalProtocolError): |
|
cs.process_event(CLIENT, Request) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(SERVER, InformationalResponse) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(SERVER, Response) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY} |
|
|
|
cs.process_event(CLIENT, EndOfMessage) |
|
cs.process_event(SERVER, EndOfMessage) |
|
assert cs.states == {CLIENT: DONE, SERVER: DONE} |
|
|
|
# State-triggered transition |
|
|
|
cs.process_event(SERVER, ConnectionClosed) |
|
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED} |
|
|
|
|
|
def test_ConnectionState_keep_alive() -> None: |
|
# keep_alive = False |
|
cs = ConnectionState() |
|
cs.process_event(CLIENT, Request) |
|
cs.process_keep_alive_disabled() |
|
cs.process_event(CLIENT, EndOfMessage) |
|
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(SERVER, Response) |
|
cs.process_event(SERVER, EndOfMessage) |
|
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE} |
|
|
|
|
|
def test_ConnectionState_keep_alive_in_DONE() -> None: |
|
# Check that if keep_alive is disabled when the CLIENT is already in DONE, |
|
# then this is sufficient to immediately trigger the DONE -> MUST_CLOSE |
|
# transition |
|
cs = ConnectionState() |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
assert cs.states[CLIENT] is DONE |
|
cs.process_keep_alive_disabled() |
|
assert cs.states[CLIENT] is MUST_CLOSE |
|
|
|
|
|
def test_ConnectionState_switch_denied() -> None: |
|
for switch_type in (_SWITCH_CONNECT, _SWITCH_UPGRADE): |
|
for deny_early in (True, False): |
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(switch_type) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, Data) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
assert switch_type in cs.pending_switch_proposals |
|
|
|
if deny_early: |
|
# before client reaches DONE |
|
cs.process_event(SERVER, Response) |
|
assert not cs.pending_switch_proposals |
|
|
|
cs.process_event(CLIENT, EndOfMessage) |
|
|
|
if deny_early: |
|
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} |
|
else: |
|
assert cs.states == { |
|
CLIENT: MIGHT_SWITCH_PROTOCOL, |
|
SERVER: SEND_RESPONSE, |
|
} |
|
|
|
cs.process_event(SERVER, InformationalResponse) |
|
assert cs.states == { |
|
CLIENT: MIGHT_SWITCH_PROTOCOL, |
|
SERVER: SEND_RESPONSE, |
|
} |
|
|
|
cs.process_event(SERVER, Response) |
|
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} |
|
assert not cs.pending_switch_proposals |
|
|
|
|
|
_response_type_for_switch = { |
|
_SWITCH_UPGRADE: InformationalResponse, |
|
_SWITCH_CONNECT: Response, |
|
None: Response, |
|
} |
|
|
|
|
|
def test_ConnectionState_protocol_switch_accepted() -> None: |
|
for switch_event in [_SWITCH_UPGRADE, _SWITCH_CONNECT]: |
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(switch_event) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, Data) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(CLIENT, EndOfMessage) |
|
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(SERVER, InformationalResponse) |
|
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} |
|
|
|
cs.process_event(SERVER, _response_type_for_switch[switch_event], switch_event) |
|
assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} |
|
|
|
|
|
def test_ConnectionState_double_protocol_switch() -> None: |
|
# CONNECT + Upgrade is legal! Very silly, but legal. So we support |
|
# it. Because sometimes doing the silly thing is easier than not. |
|
for server_switch in [None, _SWITCH_UPGRADE, _SWITCH_CONNECT]: |
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(_SWITCH_UPGRADE) |
|
cs.process_client_switch_proposal(_SWITCH_CONNECT) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} |
|
cs.process_event( |
|
SERVER, _response_type_for_switch[server_switch], server_switch |
|
) |
|
if server_switch is None: |
|
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} |
|
else: |
|
assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} |
|
|
|
|
|
def test_ConnectionState_inconsistent_protocol_switch() -> None: |
|
for client_switches, server_switch in [ |
|
([], _SWITCH_CONNECT), |
|
([], _SWITCH_UPGRADE), |
|
([_SWITCH_UPGRADE], _SWITCH_CONNECT), |
|
([_SWITCH_CONNECT], _SWITCH_UPGRADE), |
|
]: |
|
cs = ConnectionState() |
|
for client_switch in client_switches: # type: ignore[attr-defined] |
|
cs.process_client_switch_proposal(client_switch) |
|
cs.process_event(CLIENT, Request) |
|
with pytest.raises(LocalProtocolError): |
|
cs.process_event(SERVER, Response, server_switch) |
|
|
|
|
|
def test_ConnectionState_keepalive_protocol_switch_interaction() -> None: |
|
# keep_alive=False + pending_switch_proposals |
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(_SWITCH_UPGRADE) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_keep_alive_disabled() |
|
cs.process_event(CLIENT, Data) |
|
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} |
|
|
|
# the protocol switch "wins" |
|
cs.process_event(CLIENT, EndOfMessage) |
|
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} |
|
|
|
# but when the server denies the request, keep_alive comes back into play |
|
cs.process_event(SERVER, Response) |
|
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_BODY} |
|
|
|
|
|
def test_ConnectionState_reuse() -> None: |
|
cs = ConnectionState() |
|
|
|
with pytest.raises(LocalProtocolError): |
|
cs.start_next_cycle() |
|
|
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
|
|
with pytest.raises(LocalProtocolError): |
|
cs.start_next_cycle() |
|
|
|
cs.process_event(SERVER, Response) |
|
cs.process_event(SERVER, EndOfMessage) |
|
|
|
cs.start_next_cycle() |
|
assert cs.states == {CLIENT: IDLE, SERVER: IDLE} |
|
|
|
# No keepalive |
|
|
|
cs.process_event(CLIENT, Request) |
|
cs.process_keep_alive_disabled() |
|
cs.process_event(CLIENT, EndOfMessage) |
|
cs.process_event(SERVER, Response) |
|
cs.process_event(SERVER, EndOfMessage) |
|
|
|
with pytest.raises(LocalProtocolError): |
|
cs.start_next_cycle() |
|
|
|
# One side closed |
|
|
|
cs = ConnectionState() |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
cs.process_event(CLIENT, ConnectionClosed) |
|
cs.process_event(SERVER, Response) |
|
cs.process_event(SERVER, EndOfMessage) |
|
|
|
with pytest.raises(LocalProtocolError): |
|
cs.start_next_cycle() |
|
|
|
# Succesful protocol switch |
|
|
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(_SWITCH_UPGRADE) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
cs.process_event(SERVER, InformationalResponse, _SWITCH_UPGRADE) |
|
|
|
with pytest.raises(LocalProtocolError): |
|
cs.start_next_cycle() |
|
|
|
# Failed protocol switch |
|
|
|
cs = ConnectionState() |
|
cs.process_client_switch_proposal(_SWITCH_UPGRADE) |
|
cs.process_event(CLIENT, Request) |
|
cs.process_event(CLIENT, EndOfMessage) |
|
cs.process_event(SERVER, Response) |
|
cs.process_event(SERVER, EndOfMessage) |
|
|
|
cs.start_next_cycle() |
|
assert cs.states == {CLIENT: IDLE, SERVER: IDLE} |
|
|
|
|
|
def test_server_request_is_illegal() -> None: |
|
# There used to be a bug in how we handled the Request special case that |
|
# made this allowed... |
|
cs = ConnectionState() |
|
with pytest.raises(LocalProtocolError): |
|
cs.process_event(SERVER, Request)
|
|
|