examples_SUITE.erl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. %% Copyright (c) 2016, 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(examples_SUITE).
  15. -compile(export_all).
  16. -import(ct_helper, [config/2]).
  17. -import(ct_helper, [doc/1]).
  18. -import(cowboy_test, [gun_open/1]).
  19. %% ct.
  20. all() ->
  21. ct_helper:all(?MODULE).
  22. %% Remove environment variables inherited from Erlang.mk.
  23. init_per_suite(Config) ->
  24. os:unsetenv("ERLANG_MK_TMP"),
  25. os:unsetenv("APPS_DIR"),
  26. os:unsetenv("DEPS_DIR"),
  27. os:unsetenv("ERL_LIBS"),
  28. Config.
  29. end_per_suite(_) ->
  30. ok.
  31. %% Compile, start and stop releases.
  32. do_get_paths(Example0) ->
  33. Example = atom_to_list(Example0),
  34. {ok, CWD} = file:get_cwd(),
  35. Dir = CWD ++ "/../../examples/" ++ Example,
  36. Rel = Dir ++ "/_rel/" ++ Example ++ "_example/bin/" ++ Example ++ "_example",
  37. Log = Dir ++ "/_rel/" ++ Example ++ "_example/log/erlang.log.1",
  38. {Dir, Rel, Log}.
  39. do_compile_and_start(Example) ->
  40. {Dir, Rel, _} = do_get_paths(Example),
  41. %% TERM=dumb disables relx coloring.
  42. ct:log("~s~n", [os:cmd("cd " ++ Dir ++ " && make distclean && TERM=dumb make all")]),
  43. ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
  44. ct:log("~s~n", [os:cmd(Rel ++ " start")]),
  45. timer:sleep(2000),
  46. ok.
  47. do_stop(Example) ->
  48. {_, Rel, Log} = do_get_paths(Example),
  49. ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
  50. ct:log("~s~n", [element(2, file:read_file(Log))]),
  51. ok.
  52. %% Fetch a response.
  53. do_get(Transport, Protocol, Path, Config) ->
  54. do_get(Transport, Protocol, Path, [], Config).
  55. do_get(Transport, Protocol, Path, ReqHeaders, Config) ->
  56. Port = case Transport of
  57. tcp -> 8080;
  58. ssl -> 8443
  59. end,
  60. ConnPid = gun_open([{port, Port}, {type, Transport}, {protocol, Protocol}|Config]),
  61. Ref = gun:get(ConnPid, Path, ReqHeaders),
  62. case gun:await(ConnPid, Ref) of
  63. {response, nofin, Status, RespHeaders} ->
  64. {ok, Body} = gun:await_body(ConnPid, Ref),
  65. {Status, RespHeaders, Body};
  66. {response, fin, Status, RespHeaders} ->
  67. {Status, RespHeaders, <<>>}
  68. end.
  69. %% TCP and SSL Hello World.
  70. hello_world(Config) ->
  71. doc("Hello World example."),
  72. try
  73. do_compile_and_start(hello_world),
  74. do_hello_world(tcp, http, Config),
  75. do_hello_world(tcp, http2, Config)
  76. after
  77. do_stop(hello_world)
  78. end.
  79. ssl_hello_world(Config) ->
  80. doc("SSL Hello World example."),
  81. try
  82. do_compile_and_start(ssl_hello_world),
  83. do_hello_world(ssl, http, Config),
  84. do_hello_world(ssl, http2, Config)
  85. after
  86. do_stop(ssl_hello_world)
  87. end.
  88. do_hello_world(Transport, Protocol, Config) ->
  89. {200, _, <<"Hello world!">>} = do_get(Transport, Protocol, "/", Config),
  90. ok.
  91. %% Chunked Hello World.
  92. chunked_hello_world(Config) ->
  93. doc("Chunked Hello World example."),
  94. try
  95. do_compile_and_start(chunked_hello_world),
  96. do_chunked_hello_world(tcp, http, Config),
  97. do_chunked_hello_world(tcp, http2, Config)
  98. after
  99. do_stop(chunked_hello_world)
  100. end.
  101. do_chunked_hello_world(Transport, Protocol, Config) ->
  102. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  103. Ref = gun:get(ConnPid, "/"),
  104. {response, nofin, 200, _} = gun:await(ConnPid, Ref),
  105. %% We expect to receive a chunk every second, three total.
  106. {data, nofin, <<"Hello\r\n">>} = gun:await(ConnPid, Ref, 2000),
  107. {data, nofin, <<"World\r\n">>} = gun:await(ConnPid, Ref, 2000),
  108. {data, IsFin, <<"Chunked!\r\n">>} = gun:await(ConnPid, Ref, 2000),
  109. %% We may get an extra empty chunk (last chunk for HTTP/1.1,
  110. %% empty DATA frame with the FIN bit set for HTTP/2).
  111. case IsFin of
  112. fin -> ok;
  113. nofin ->
  114. {data, fin, <<>>} = gun:await(ConnPid, Ref, 500),
  115. ok
  116. end.
  117. %% Cookie.
  118. cookie(Config) ->
  119. doc("Cookie example."),
  120. try
  121. do_compile_and_start(cookie),
  122. do_cookie(tcp, http, Config),
  123. do_cookie(tcp, http2, Config)
  124. after
  125. do_stop(cookie)
  126. end.
  127. do_cookie(Transport, Protocol, Config) ->
  128. {200, _, One} = do_get(Transport, Protocol, "/", Config),
  129. {200, _, Two} = do_get(Transport, Protocol, "/", [{<<"cookie">>, <<"server=abcdef">>}], Config),
  130. true = One =/= Two,
  131. ok.
  132. %% Echo GET.
  133. echo_get(Config) ->
  134. doc("GET parameter echo example."),
  135. try
  136. do_compile_and_start(echo_get),
  137. do_echo_get(tcp, http, Config),
  138. do_echo_get(tcp, http2, Config)
  139. after
  140. do_stop(echo_get)
  141. end.
  142. do_echo_get(Transport, Protocol, Config) ->
  143. {200, _, <<"this is fun">>} = do_get(Transport, Protocol, "/?echo=this+is+fun", Config),
  144. ok.
  145. %% Echo POST.
  146. echo_post(Config) ->
  147. doc("POST parameter echo example."),
  148. try
  149. do_compile_and_start(echo_post),
  150. do_echo_post(tcp, http, Config),
  151. do_echo_post(tcp, http2, Config)
  152. after
  153. do_stop(echo_post)
  154. end.
  155. do_echo_post(Transport, Protocol, Config) ->
  156. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  157. Ref = gun:post(ConnPid, "/", [
  158. {<<"content-type">>, <<"application/octet-stream">>}
  159. ], <<"echo=this+is+fun">>),
  160. {response, nofin, 200, _} = gun:await(ConnPid, Ref),
  161. {ok, <<"this is fun">>} = gun:await_body(ConnPid, Ref),
  162. ok.
  163. %% Eventsource.
  164. eventsource(Config) ->
  165. doc("Eventsource example."),
  166. try
  167. do_compile_and_start(eventsource),
  168. do_eventsource(tcp, http, Config),
  169. do_eventsource(tcp, http2, Config)
  170. after
  171. do_stop(eventsource)
  172. end.
  173. do_eventsource(Transport, Protocol, Config) ->
  174. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  175. Ref = gun:get(ConnPid, "/eventsource"),
  176. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  177. {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  178. %% Receive a few events.
  179. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  180. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  181. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  182. gun:close(ConnPid).
  183. %% REST Hello World.
  184. rest_hello_world(Config) ->
  185. doc("REST Hello World example."),
  186. try
  187. do_compile_and_start(rest_hello_world),
  188. do_rest_hello_world(tcp, http, Config),
  189. do_rest_hello_world(tcp, http2, Config)
  190. after
  191. do_stop(rest_hello_world)
  192. end.
  193. do_rest_hello_world(Transport, Protocol, Config) ->
  194. << "<html>", _/bits >> = do_rest_get(Transport, Protocol,
  195. "/", undefined, undefined, Config),
  196. << "REST Hello World as text!" >> = do_rest_get(Transport, Protocol,
  197. "/", <<"text/plain">>, undefined, Config),
  198. << "{\"rest\": \"Hello World!\"}" >> = do_rest_get(Transport, Protocol,
  199. "/", <<"application/json">>, undefined, Config),
  200. not_acceptable = do_rest_get(Transport, Protocol,
  201. "/", <<"text/css">>, undefined, Config),
  202. ok.
  203. do_rest_get(Transport, Protocol, Path, Accept, Auth, Config) ->
  204. ReqHeaders0 = case Accept of
  205. undefined -> [];
  206. _ -> [{<<"accept">>, Accept}]
  207. end,
  208. ReqHeaders = case Auth of
  209. undefined -> ReqHeaders0;
  210. _ -> [{<<"authorization">>, [<<"Basic ">>, base64:encode(Auth)]}|ReqHeaders0]
  211. end,
  212. case do_get(Transport, Protocol, Path, ReqHeaders, Config) of
  213. {200, RespHeaders, Body} ->
  214. Accept = case Accept of
  215. undefined -> undefined;
  216. _ ->
  217. {_, ContentType} = lists:keyfind(<<"content-type">>, 1, RespHeaders),
  218. ContentType
  219. end,
  220. Body;
  221. {401, _, _} ->
  222. unauthorized;
  223. {406, _, _} ->
  224. not_acceptable
  225. end.
  226. %% REST basic auth.
  227. rest_basic_auth(Config) ->
  228. doc("REST basic authorization example."),
  229. try
  230. do_compile_and_start(rest_basic_auth),
  231. do_rest_basic_auth(tcp, http, Config),
  232. do_rest_basic_auth(tcp, http2, Config)
  233. after
  234. do_stop(rest_basic_auth)
  235. end.
  236. do_rest_basic_auth(Transport, Protocol, Config) ->
  237. unauthorized = do_rest_get(Transport, Protocol, "/", undefined, undefined, Config),
  238. <<"Hello, Alladin!\n">> = do_rest_get(Transport, Protocol, "/", undefined, "Alladin:open sesame", Config),
  239. ok.
  240. %% REST pastebin.
  241. rest_pastebin(Config) ->
  242. doc("REST pastebin example."),
  243. try
  244. do_compile_and_start(rest_pastebin),
  245. do_rest_pastebin(tcp, http, Config),
  246. do_rest_pastebin(tcp, http2, Config)
  247. after
  248. do_stop(rest_pastebin)
  249. end.
  250. do_rest_pastebin(Transport, Protocol, Config) ->
  251. %% Existing files.
  252. _ = do_rest_get(Transport, Protocol, "/", <<"text/html">>, undefined, Config),
  253. _ = do_rest_get(Transport, Protocol, "/", <<"text/plain">>, undefined, Config),
  254. %% Use POST to upload a new file and download it back.
  255. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  256. Ref = gun:post(ConnPid, "/", [
  257. {<<"content-type">>, <<"application/x-www-form-urlencoded">>}
  258. ], <<"paste=this+is+fun">>),
  259. %% @todo Not too happy about 303 here,
  260. %% will need to revisit this example.
  261. {response, _, 303, Headers} = gun:await(ConnPid, Ref),
  262. {_, Location} = lists:keyfind(<<"location">>, 1, Headers),
  263. <<"this is fun">> = do_rest_get(Transport, Protocol, Location, <<"text/plain">>, undefined, Config),
  264. << "<!DOCTYPE html><html>", _/bits >>
  265. = do_rest_get(Transport, Protocol, Location, <<"text/html">>, undefined, Config),
  266. ok.
  267. %% File server.
  268. file_server(Config) ->
  269. doc("File server example with directory listing."),
  270. try
  271. do_compile_and_start(file_server),
  272. do_file_server(tcp, http, Config),
  273. do_file_server(tcp, http2, Config)
  274. after
  275. do_stop(file_server)
  276. end.
  277. do_file_server(Transport, Protocol, Config) ->
  278. %% Directory.
  279. {200, DirHeaders, <<"<!DOCTYPE html><html>", _/bits >>} = do_get(Transport, Protocol, "/", Config),
  280. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, DirHeaders),
  281. _ = do_rest_get(Transport, Protocol, "/", <<"application/json">>, undefined, Config),
  282. %% Files.
  283. {200, _, _} = do_get(Transport, Protocol, "/small.mp4", Config),
  284. {200, _, _} = do_get(Transport, Protocol, "/small.ogv", Config),
  285. {200, _, _} = do_get(Transport, Protocol, "/test.txt", Config),
  286. {200, _, _} = do_get(Transport, Protocol, "/video.html", Config),
  287. ok.
  288. %% Markdown middleware.
  289. markdown_middleware(Config) ->
  290. doc("Markdown middleware example."),
  291. try
  292. do_compile_and_start(markdown_middleware),
  293. do_markdown_middleware(tcp, http, Config),
  294. do_markdown_middleware(tcp, http2, Config)
  295. after
  296. do_stop(markdown_middleware)
  297. end.
  298. do_markdown_middleware(Transport, Protocol, Config) ->
  299. {200, Headers, <<"<h1>", _/bits >>} = do_get(Transport, Protocol, "/video.html", Config),
  300. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  301. ok.
  302. %% Upload.
  303. upload(Config) ->
  304. doc("Upload example."),
  305. try
  306. do_compile_and_start(upload),
  307. do_upload(tcp, http, Config),
  308. do_upload(tcp, http2, Config)
  309. after
  310. do_stop(upload)
  311. end.
  312. do_upload(Transport, Protocol, Config) ->
  313. {200, _, << "<html>", _/bits >>} = do_get(Transport, Protocol, "/", Config),
  314. %% Use POST to upload a file using multipart.
  315. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  316. Ref = gun:post(ConnPid, "/upload", [
  317. {<<"content-type">>, <<"multipart/form-data;boundary=deadbeef">>}
  318. ], <<
  319. "--deadbeef\r\n"
  320. "Content-Disposition: form-data; name=\"inputfile\"; filename=\"test.txt\"\r\n"
  321. "Content-Type: text/plain\r\n"
  322. "\r\n"
  323. "Cowboy upload example!\r\n"
  324. "--deadbeef--">>),
  325. {response, fin, 204, _} = gun:await(ConnPid, Ref),
  326. ok.
  327. %% Websocket.
  328. websocket(_) ->
  329. doc("Websocket example."),
  330. try
  331. do_compile_and_start(websocket),
  332. %% We can only initiate a Websocket connection from HTTP/1.1.
  333. {ok, Pid} = gun:open("127.0.0.1", 8080, #{protocols => [http], retry => 0}),
  334. {ok, http} = gun:await_up(Pid),
  335. _ = monitor(process, Pid),
  336. gun:ws_upgrade(Pid, "/websocket", [], #{compress => true}),
  337. receive
  338. {gun_ws_upgrade, Pid, ok, _} ->
  339. ok;
  340. Msg1 ->
  341. exit({connection_failed, Msg1})
  342. end,
  343. gun:ws_send(Pid, {text, <<"hello">>}),
  344. receive
  345. {gun_ws, Pid, {text, <<"That's what she said! hello">>}} ->
  346. ok;
  347. Msg2 ->
  348. exit({receive_failed, Msg2})
  349. end,
  350. gun:ws_send(Pid, close)
  351. after
  352. do_stop(websocket)
  353. end.