ws_handler_SUITE.erl 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. %% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(ws_handler_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(cowboy_test, [gun_open/1]).
  20. -import(cowboy_test, [gun_down/1]).
  21. %% ct.
  22. all() ->
  23. [{group, ws}, {group, ws_hibernate}].
  24. %% @todo Test against HTTP/2 too.
  25. groups() ->
  26. AllTests = ct_helper:all(?MODULE),
  27. [{ws, [parallel], AllTests}, {ws_hibernate, [parallel], AllTests}].
  28. init_per_group(Name, Config) ->
  29. cowboy_test:init_http(Name, #{
  30. env => #{dispatch => init_dispatch(Name)}
  31. }, Config).
  32. end_per_group(Name, _) ->
  33. cowboy:stop_listener(Name).
  34. %% Dispatch configuration.
  35. init_dispatch(Name) ->
  36. RunOrHibernate = case Name of
  37. ws -> run;
  38. ws_hibernate -> hibernate
  39. end,
  40. cowboy_router:compile([{'_', [
  41. {"/init", ws_init_commands_h, RunOrHibernate},
  42. {"/handle", ws_handle_commands_h, RunOrHibernate},
  43. {"/info", ws_info_commands_h, RunOrHibernate},
  44. {"/active", ws_active_commands_h, RunOrHibernate}
  45. ]}]).
  46. %% Support functions for testing using Gun.
  47. gun_open_ws(Config, Path, Commands) ->
  48. ConnPid = gun_open(Config),
  49. StreamRef = gun:ws_upgrade(ConnPid, Path, [
  50. {<<"x-commands">>, base64:encode(term_to_binary(Commands))}
  51. ]),
  52. receive
  53. {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
  54. {ok, ConnPid, StreamRef};
  55. {gun_response, ConnPid, _, _, Status, Headers} ->
  56. exit({ws_upgrade_failed, Status, Headers});
  57. {gun_error, ConnPid, StreamRef, Reason} ->
  58. exit({ws_upgrade_failed, Reason})
  59. after 1000 ->
  60. error(timeout)
  61. end.
  62. receive_ws(ConnPid, StreamRef) ->
  63. receive
  64. {gun_ws, ConnPid, StreamRef, Frame} ->
  65. {ok, Frame}
  66. after 1000 ->
  67. {error, timeout}
  68. end.
  69. ensure_handle_is_called(ConnPid, "/handle") ->
  70. gun:ws_send(ConnPid, {text, <<"Necessary to trigger websocket_handle/2.">>});
  71. ensure_handle_is_called(_, _) ->
  72. ok.
  73. %% Tests.
  74. websocket_init_nothing(Config) ->
  75. doc("Nothing happens when websocket_init/1 returns no commands."),
  76. do_nothing(Config, "/init").
  77. websocket_handle_nothing(Config) ->
  78. doc("Nothing happens when websocket_handle/2 returns no commands."),
  79. do_nothing(Config, "/handle").
  80. websocket_info_nothing(Config) ->
  81. doc("Nothing happens when websocket_info/2 returns no commands."),
  82. do_nothing(Config, "/info").
  83. do_nothing(Config, Path) ->
  84. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, []),
  85. ensure_handle_is_called(ConnPid, Path),
  86. {error, timeout} = receive_ws(ConnPid, StreamRef),
  87. ok.
  88. websocket_init_invalid(Config) ->
  89. doc("The connection must be closed when websocket_init/1 returns an invalid command."),
  90. do_invalid(Config, "/init").
  91. websocket_handle_invalid(Config) ->
  92. doc("The connection must be closed when websocket_handle/2 returns an invalid command."),
  93. do_invalid(Config, "/init").
  94. websocket_info_invalid(Config) ->
  95. doc("The connection must be closed when websocket_info/2 returns an invalid command."),
  96. do_invalid(Config, "/info").
  97. do_invalid(Config, Path) ->
  98. {ok, ConnPid, _} = gun_open_ws(Config, Path, bad),
  99. ensure_handle_is_called(ConnPid, Path),
  100. gun_down(ConnPid).
  101. websocket_init_one_frame(Config) ->
  102. doc("A single frame is received when websocket_init/1 returns it as a command."),
  103. do_one_frame(Config, "/init").
  104. websocket_handle_one_frame(Config) ->
  105. doc("A single frame is received when websocket_handle/2 returns it as a command."),
  106. do_one_frame(Config, "/handle").
  107. websocket_info_one_frame(Config) ->
  108. doc("A single frame is received when websocket_info/2 returns it as a command."),
  109. do_one_frame(Config, "/info").
  110. do_one_frame(Config, Path) ->
  111. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  112. {text, <<"One frame!">>}
  113. ]),
  114. ensure_handle_is_called(ConnPid, Path),
  115. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  116. ok.
  117. websocket_init_many_frames(Config) ->
  118. doc("Multiple frames are received when websocket_init/1 returns them as commands."),
  119. do_many_frames(Config, "/init").
  120. websocket_handle_many_frames(Config) ->
  121. doc("Multiple frames are received when websocket_handle/2 returns them as commands."),
  122. do_many_frames(Config, "/handle").
  123. websocket_info_many_frames(Config) ->
  124. doc("Multiple frames are received when websocket_info/2 returns them as commands."),
  125. do_many_frames(Config, "/info").
  126. do_many_frames(Config, Path) ->
  127. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  128. {text, <<"One frame!">>},
  129. {binary, <<"Two frames!">>}
  130. ]),
  131. ensure_handle_is_called(ConnPid, Path),
  132. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  133. {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
  134. ok.
  135. websocket_init_close_frame(Config) ->
  136. doc("A single close frame is received when websocket_init/1 returns it as a command."),
  137. do_close_frame(Config, "/init").
  138. websocket_handle_close_frame(Config) ->
  139. doc("A single close frame is received when websocket_handle/2 returns it as a command."),
  140. do_close_frame(Config, "/handle").
  141. websocket_info_close_frame(Config) ->
  142. doc("A single close frame is received when websocket_info/2 returns it as a command."),
  143. do_close_frame(Config, "/info").
  144. do_close_frame(Config, Path) ->
  145. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [close]),
  146. ensure_handle_is_called(ConnPid, Path),
  147. {ok, close} = receive_ws(ConnPid, StreamRef),
  148. gun_down(ConnPid).
  149. websocket_init_many_frames_then_close_frame(Config) ->
  150. doc("Multiple frames are received followed by a close frame "
  151. "when websocket_init/1 returns them as commands."),
  152. do_many_frames_then_close_frame(Config, "/init").
  153. websocket_handle_many_frames_then_close_frame(Config) ->
  154. doc("Multiple frames are received followed by a close frame "
  155. "when websocket_handle/2 returns them as commands."),
  156. do_many_frames_then_close_frame(Config, "/handle").
  157. websocket_info_many_frames_then_close_frame(Config) ->
  158. doc("Multiple frames are received followed by a close frame "
  159. "when websocket_info/2 returns them as commands."),
  160. do_many_frames_then_close_frame(Config, "/info").
  161. do_many_frames_then_close_frame(Config, Path) ->
  162. {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [
  163. {text, <<"One frame!">>},
  164. {binary, <<"Two frames!">>},
  165. close
  166. ]),
  167. ensure_handle_is_called(ConnPid, Path),
  168. {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef),
  169. {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef),
  170. {ok, close} = receive_ws(ConnPid, StreamRef),
  171. gun_down(ConnPid).
  172. websocket_active_false(Config) ->
  173. doc("The {active, false} command stops receiving data from the socket. "
  174. "The {active, true} command reenables it."),
  175. {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/active", []),
  176. gun:ws_send(ConnPid, {text, <<"Not received until the handler enables active again.">>}),
  177. {error, timeout} = receive_ws(ConnPid, StreamRef),
  178. {ok, {text, <<"Not received until the handler enables active again.">>}}
  179. = receive_ws(ConnPid, StreamRef),
  180. ok.