test_state.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import pytest
  2. from .._events import (
  3. ConnectionClosed,
  4. Data,
  5. EndOfMessage,
  6. Event,
  7. InformationalResponse,
  8. Request,
  9. Response,
  10. )
  11. from .._state import (
  12. _SWITCH_CONNECT,
  13. _SWITCH_UPGRADE,
  14. CLIENT,
  15. CLOSED,
  16. ConnectionState,
  17. DONE,
  18. IDLE,
  19. MIGHT_SWITCH_PROTOCOL,
  20. MUST_CLOSE,
  21. SEND_BODY,
  22. SEND_RESPONSE,
  23. SERVER,
  24. SWITCHED_PROTOCOL,
  25. )
  26. from .._util import LocalProtocolError
  27. def test_ConnectionState() -> None:
  28. cs = ConnectionState()
  29. # Basic event-triggered transitions
  30. assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
  31. cs.process_event(CLIENT, Request)
  32. # The SERVER-Request special case:
  33. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  34. # Illegal transitions raise an error and nothing happens
  35. with pytest.raises(LocalProtocolError):
  36. cs.process_event(CLIENT, Request)
  37. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  38. cs.process_event(SERVER, InformationalResponse)
  39. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  40. cs.process_event(SERVER, Response)
  41. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY}
  42. cs.process_event(CLIENT, EndOfMessage)
  43. cs.process_event(SERVER, EndOfMessage)
  44. assert cs.states == {CLIENT: DONE, SERVER: DONE}
  45. # State-triggered transition
  46. cs.process_event(SERVER, ConnectionClosed)
  47. assert cs.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED}
  48. def test_ConnectionState_keep_alive() -> None:
  49. # keep_alive = False
  50. cs = ConnectionState()
  51. cs.process_event(CLIENT, Request)
  52. cs.process_keep_alive_disabled()
  53. cs.process_event(CLIENT, EndOfMessage)
  54. assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_RESPONSE}
  55. cs.process_event(SERVER, Response)
  56. cs.process_event(SERVER, EndOfMessage)
  57. assert cs.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE}
  58. def test_ConnectionState_keep_alive_in_DONE() -> None:
  59. # Check that if keep_alive is disabled when the CLIENT is already in DONE,
  60. # then this is sufficient to immediately trigger the DONE -> MUST_CLOSE
  61. # transition
  62. cs = ConnectionState()
  63. cs.process_event(CLIENT, Request)
  64. cs.process_event(CLIENT, EndOfMessage)
  65. assert cs.states[CLIENT] is DONE
  66. cs.process_keep_alive_disabled()
  67. assert cs.states[CLIENT] is MUST_CLOSE
  68. def test_ConnectionState_switch_denied() -> None:
  69. for switch_type in (_SWITCH_CONNECT, _SWITCH_UPGRADE):
  70. for deny_early in (True, False):
  71. cs = ConnectionState()
  72. cs.process_client_switch_proposal(switch_type)
  73. cs.process_event(CLIENT, Request)
  74. cs.process_event(CLIENT, Data)
  75. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  76. assert switch_type in cs.pending_switch_proposals
  77. if deny_early:
  78. # before client reaches DONE
  79. cs.process_event(SERVER, Response)
  80. assert not cs.pending_switch_proposals
  81. cs.process_event(CLIENT, EndOfMessage)
  82. if deny_early:
  83. assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
  84. else:
  85. assert cs.states == {
  86. CLIENT: MIGHT_SWITCH_PROTOCOL,
  87. SERVER: SEND_RESPONSE,
  88. }
  89. cs.process_event(SERVER, InformationalResponse)
  90. assert cs.states == {
  91. CLIENT: MIGHT_SWITCH_PROTOCOL,
  92. SERVER: SEND_RESPONSE,
  93. }
  94. cs.process_event(SERVER, Response)
  95. assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
  96. assert not cs.pending_switch_proposals
  97. _response_type_for_switch = {
  98. _SWITCH_UPGRADE: InformationalResponse,
  99. _SWITCH_CONNECT: Response,
  100. None: Response,
  101. }
  102. def test_ConnectionState_protocol_switch_accepted() -> None:
  103. for switch_event in [_SWITCH_UPGRADE, _SWITCH_CONNECT]:
  104. cs = ConnectionState()
  105. cs.process_client_switch_proposal(switch_event)
  106. cs.process_event(CLIENT, Request)
  107. cs.process_event(CLIENT, Data)
  108. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  109. cs.process_event(CLIENT, EndOfMessage)
  110. assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
  111. cs.process_event(SERVER, InformationalResponse)
  112. assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
  113. cs.process_event(SERVER, _response_type_for_switch[switch_event], switch_event)
  114. assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
  115. def test_ConnectionState_double_protocol_switch() -> None:
  116. # CONNECT + Upgrade is legal! Very silly, but legal. So we support
  117. # it. Because sometimes doing the silly thing is easier than not.
  118. for server_switch in [None, _SWITCH_UPGRADE, _SWITCH_CONNECT]:
  119. cs = ConnectionState()
  120. cs.process_client_switch_proposal(_SWITCH_UPGRADE)
  121. cs.process_client_switch_proposal(_SWITCH_CONNECT)
  122. cs.process_event(CLIENT, Request)
  123. cs.process_event(CLIENT, EndOfMessage)
  124. assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
  125. cs.process_event(
  126. SERVER, _response_type_for_switch[server_switch], server_switch
  127. )
  128. if server_switch is None:
  129. assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
  130. else:
  131. assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
  132. def test_ConnectionState_inconsistent_protocol_switch() -> None:
  133. for client_switches, server_switch in [
  134. ([], _SWITCH_CONNECT),
  135. ([], _SWITCH_UPGRADE),
  136. ([_SWITCH_UPGRADE], _SWITCH_CONNECT),
  137. ([_SWITCH_CONNECT], _SWITCH_UPGRADE),
  138. ]:
  139. cs = ConnectionState()
  140. for client_switch in client_switches: # type: ignore[attr-defined]
  141. cs.process_client_switch_proposal(client_switch)
  142. cs.process_event(CLIENT, Request)
  143. with pytest.raises(LocalProtocolError):
  144. cs.process_event(SERVER, Response, server_switch)
  145. def test_ConnectionState_keepalive_protocol_switch_interaction() -> None:
  146. # keep_alive=False + pending_switch_proposals
  147. cs = ConnectionState()
  148. cs.process_client_switch_proposal(_SWITCH_UPGRADE)
  149. cs.process_event(CLIENT, Request)
  150. cs.process_keep_alive_disabled()
  151. cs.process_event(CLIENT, Data)
  152. assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
  153. # the protocol switch "wins"
  154. cs.process_event(CLIENT, EndOfMessage)
  155. assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
  156. # but when the server denies the request, keep_alive comes back into play
  157. cs.process_event(SERVER, Response)
  158. assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_BODY}
  159. def test_ConnectionState_reuse() -> None:
  160. cs = ConnectionState()
  161. with pytest.raises(LocalProtocolError):
  162. cs.start_next_cycle()
  163. cs.process_event(CLIENT, Request)
  164. cs.process_event(CLIENT, EndOfMessage)
  165. with pytest.raises(LocalProtocolError):
  166. cs.start_next_cycle()
  167. cs.process_event(SERVER, Response)
  168. cs.process_event(SERVER, EndOfMessage)
  169. cs.start_next_cycle()
  170. assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
  171. # No keepalive
  172. cs.process_event(CLIENT, Request)
  173. cs.process_keep_alive_disabled()
  174. cs.process_event(CLIENT, EndOfMessage)
  175. cs.process_event(SERVER, Response)
  176. cs.process_event(SERVER, EndOfMessage)
  177. with pytest.raises(LocalProtocolError):
  178. cs.start_next_cycle()
  179. # One side closed
  180. cs = ConnectionState()
  181. cs.process_event(CLIENT, Request)
  182. cs.process_event(CLIENT, EndOfMessage)
  183. cs.process_event(CLIENT, ConnectionClosed)
  184. cs.process_event(SERVER, Response)
  185. cs.process_event(SERVER, EndOfMessage)
  186. with pytest.raises(LocalProtocolError):
  187. cs.start_next_cycle()
  188. # Succesful protocol switch
  189. cs = ConnectionState()
  190. cs.process_client_switch_proposal(_SWITCH_UPGRADE)
  191. cs.process_event(CLIENT, Request)
  192. cs.process_event(CLIENT, EndOfMessage)
  193. cs.process_event(SERVER, InformationalResponse, _SWITCH_UPGRADE)
  194. with pytest.raises(LocalProtocolError):
  195. cs.start_next_cycle()
  196. # Failed protocol switch
  197. cs = ConnectionState()
  198. cs.process_client_switch_proposal(_SWITCH_UPGRADE)
  199. cs.process_event(CLIENT, Request)
  200. cs.process_event(CLIENT, EndOfMessage)
  201. cs.process_event(SERVER, Response)
  202. cs.process_event(SERVER, EndOfMessage)
  203. cs.start_next_cycle()
  204. assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
  205. def test_server_request_is_illegal() -> None:
  206. # There used to be a bug in how we handled the Request special case that
  207. # made this allowed...
  208. cs = ConnectionState()
  209. with pytest.raises(LocalProtocolError):
  210. cs.process_event(SERVER, Request)