examples_SUITE.erl 13 KB

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