tank-bohr / bookish_spork Goto Github PK
View Code? Open in Web Editor NEWErlang library for testing http requests
License: Apache License 2.0
Erlang library for testing http requests
License: Apache License 2.0
Здравствуйте, Алексей.
Заранее извиняюсь, что пишу в github.
Я Дмитрий. 27 августа на вашу почту отправлял вакансии Ruby on Rails dev.
Хочу узнать вы готовы рассмотреть предложение о работе?
With OTP 27 now released I'm going around my codebase and trying to replace jsone with the new stdlib json library.
One difference is that the stdlib json library returns an iodata from json:encode.
Unfortunately, bookish_spork currently doesn't support iodata bodies, leading to uncomfortable use of iolist_to_binary in each response.
It would be nice if the response body supported iodata. Looking at the code, it should be easy, and I'm probably going to submit a PR in a day or two.
:bookish_spork.start_server()
:bookish_spork.stub_request()
Task.async(fn -> some_http_req_to_bookish_spork() end)
:bookish_spork.capture_request() # => %{:error, :no_request}
Need to add ability for capture_request
to wait for async requests to complete
I reproduced this error by adding such a test (examples/chuck_ex/test/chuck_norris_api_test.exs):
test "retrieves two random jokes in different processes" do
:bookish_spork.stub_request([200, %{}, "{\"value\": \"First joke.\"}"])
:bookish_spork.stub_request([200, %{}, "{\"value\": \"Second joke.\"}"])
Task.start(fn -> ChuckNorrisApi.random() end)
Task.start(fn -> ChuckNorrisApi.random() end)
Process.sleep(500)
{:ok, _request} = :bookish_spork.capture_request()
{:ok, _request} = :bookish_spork.capture_request()
{:error, _request} = :bookish_spork.capture_request()
end
I get this error when checking the second capture_request:
1) test retrieves two random jokes in different processes (ChuckNorrisApiTest)
test/chuck_norris_api_test.exs:30
** (MatchError) no match of right hand side value: {:error, :no_request}
code: {:ok, _request} = :bookish_spork.capture_request()
stacktrace:
test/chuck_norris_api_test.exs:40: (test)
If I forcibly add an http header {"Connection", "close"}
to an http request, then the test passes.
examples/chuck_ex/lib/chuck_norris_api.ex
defp request(url) do
HTTPoison.get!(url, [{"Connection", "close"}]).body |> Poison.decode! |> Map.fetch!("value")
end
bookish_spork:start_server([ssl]),
bookish_spork:stub_request(),
hackney:get("https://localhost:32002", [], <<>>, [{ssl_options, [{verify, verify_none}]}])
bookish_spork:capture_request()
2019-08-23T09:06:06.714141+03:00 error: ** State machine <0.954.0> terminating, ** Last event = {{timeout,recv},timeout}, ** When server state = [{data,[{"State",{connection,{state,{static_env,client,gen_tcp,tls_connection,tcp,tcp_closed,tcp_error,"localhost",32002,#Port<0.100>,#Ref<0.3453212124.1051066371.98471>,#Ref<0.3453212124.1051066371.98479>,ssl_session_cache,{ssl_crl_cache,{{#Ref<0.3453212124.1051066371.98474>,#Ref<0.3453212124.1051066371.98475>},[]}},{#Ref<0.3453212124.1051066371.98472>,#Ref<0.3453212124.1051066371.98473>},#Ref<0.3453212124.1050935299.103344>,undefined},"***",{ssl_options,tls,[{3,3},{3,2},{3,1},{3,0}],verify_none,{#Fun<ssl.8.129595827>,[]},#Fun<ssl.9.129595827>,false,false,undefined,1,<<>>,"***",<<>>,"***","***","***",<<>>,"***",undefined,undefined,"***","***",[<<"À,">>,<<"À0">>,<<"À$">>,<<"À(">>,<<"À\b">>,<<"À.">>,<<"À2">>,<<"À&">>,<<"À*">>,<<0,163>>,<<0,106>>,<<0,157>>,<<0,61>>,<<"À+">>,<<"À/">>,<<"À#">>,<<"À'">>,<<"À-">>,<<"À1">>,<<"À%">>,<<"À)">>,<<0,162>>,<<0,64>>,<<0,156>>,<<0,60>>,<<"À\n">>,<<192,20>>,<<0,56>>,<<192,5>>,<<192,15>>,<<0,53>>,<<"À\t">>,<<192,19>>,<<0,50>>,<<192,4>>,<<192,14>>,<<0,47>>],undefined,true,268435456,true,undefined,infinity,false,undefined,undefined,undefined,undefined,true,"localhost",[],undefined,undefined,true,one_n_minus_one,false,false,{ssl_crl_cache,{internal,[]}},[{sha512,ecdsa},{sha512,rsa},{sha384,ecdsa},{sha384,rsa},{sha256,ecdsa},{sha256,rsa},{sha224,ecdsa},{sha224,rsa},{sha,ecdsa},{sha,rsa},{sha,dsa}],{elliptic_curves,[{1,3,132,0,39},{1,3,132,0,38},{1,3,132,0,35},{1,3,36,3,3,2,8,1,1,13},{1,3,132,0,36},{1,3,132,0,37},{1,3,36,3,3,2,8,1,1,11},{1,3,132,0,34},{1,3,132,0,16},{1,3,132,0,17},{1,3,36,3,3,2,8,1,1,7},{1,3,132,0,10},{1,2,840,10045,3,1,7},{1,3,132,0,3},{1,3,132,0,26},{1,3,132,0,27},{1,3,132,0,32},{1,3,132,0,33},{1,3,132,0,24},{1,3,132,0,25},{1,3,132,0,31},{1,2,840,10045,3,1,1},{1,3,132,0,1},{1,3,132,0,2},{1,3,132,0,15},{1,3,132,0,9},{1,3,132,0,8},{1,3,132,0,30}]},undefined,262144,full,[]},{socket_options,binary,raw,0,0,false},"***","***",false,#{active_n => 100,active_n_toggle => false,sender => <0.953.0>},"***","***","***","***",undefined,undefined}}}]}], ** Reason for termination = error:{bad_action_from_state_function,{reply,undefined,{error,timeout}}}, ** Callback mode = state_functions, ** Stacktrace =, ** [{gen_statem,parse_actions_reply,7,[{file,"gen_statem.erl"},{line,1290}]},{gen_statem,loop_event_actions_list,10,[{file,"gen_statem.erl"},{line,1206}]},{tls_connection,init,1,[{file,"tls_connection.erl"},{line,133}]},{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,249}]}]
2019-08-23T09:06:06.719806+03:00 notice: TLS server: In state connection received CLIENT ALERT: Fatal - Internal Error
2019-08-23T09:06:06.720099+03:00 error: Error in process <0.814.0> with exit value:, {{case_clause,{error,{tls_alert,{internal_error,"received SERVER ALERT: Fatal - Internal Error"}}}},[{bookish_spork_server,read_from_socket,3,[{file,"/Users/xoma/repos/basin/_checkouts/bookish_spork/src/bookish_spork_server.erl"},{line,167}]},{bookish_spork_server,handle_connection,3,[{file,"/Users/xoma/repos/basin/_checkouts/bookish_spork/src/bookish_spork_server.erl"},{line,137}]},{bookish_spork_server,'-accept/2-AcceptorFun/0-0-',2,[{file,"/Users/xoma/repos/basin/_checkouts/bookish_spork/src/bookish_spork_server.erl"},{line,130}]}]}
2019-08-23T09:06:06.728731+03:00 error: crasher: initial call: tls_connection:init/1, pid: <0.954.0>, registered_name: [], error: {{bad_action_from_state_function,{reply,undefined,{error,timeout}}},[{gen_statem,parse_actions_reply,7,[{file,"gen_statem.erl"},{line,1290}]},{gen_statem,loop_event_actions_list,10,[{file,"gen_statem.erl"},{line,1206}]},{tls_connection,init,1,[{file,"tls_connection.erl"},{line,133}]},{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,249}]}]}, ancestors: [tls_connection_sup,ssl_connection_sup,ssl_sup,<0.80.0>], message_queue_len: 0, messages: [], links: [<0.953.0>,<0.86.0>], dictionary: [{ssl_pem_cache,ssl_pem_cache},{ssl_manager,ssl_manager}], trap_exit: true, status: running, heap_size: 10958, stack_size: 27, reductions: 31716; neighbours: neighbour: pid: <0.953.0>, registered_name: [], initial call: tls_sender:init/1, current_function: {gen_statem,loop_receive,3}, ancestors: [<0.949.0>], message_queue_len: 0, links: [<0.954.0>], trap_exit: false, status: waiting, heap_size: 376, stack_size: 12, reductions: 374, current_stacktrace: [{gen_statem,loop_receive,3,[{file,"gen_statem.erl"},{line,880}]},{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,249}]}]
2019-08-23T09:06:06.729595+03:00 error: supervisor: {local,tls_connection_sup}, errorContext: child_terminated, reason: {{bad_action_from_state_function,{reply,undefined,{error,timeout}}},[{gen_statem,parse_actions_reply,7,[{file,"gen_statem.erl"},{line,1290}]},{gen_statem,loop_event_actions_list,10,[{file,"gen_statem.erl"},{line,1206}]},{tls_connection,init,1,[{file,"tls_connection.erl"},{line,133}]},{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,249}]}]}, offender: [{pid,<0.954.0>},{id,undefined},{mfargs,{tls_connection,start_link,undefined}},{restart_type,temporary},{shutdown,4000},{child_type,worker}]
bookish_spork/src/bookish_spork_response.erl
Lines 91 to 99 in 2b27089
bookish_spork/src/bookish_spork_handler.erl
Lines 77 to 106 in bc3ec4a
See
Here's a failing test case:
ssl_post_test(_Config) ->
ok = bookish_spork:stub_request(),
RequestBody = <<"{\"name\": \"John Doe\", \"email\": \"[email protected]\"}">>,
{ok, {{"HTTP/1.1", 204, "No Content"}, _, _}} = httpc:request(post, {
"https://localhost:32002/secure",
[{"Connection", "close"}],
"application/json",
RequestBody
}, [], []),
{ok, _Request} = bookish_spork:capture_request().
Here's the relevant stacktrace:
current_stacktrace: [{prim_inet,accept0,3,[]},
{inet_tcp,accept,1,[{file,"inet_tcp.erl"},{line,173}]},
{bookish_spork_transport,accept,1,
[{file,
"/Users/linus/Code/bookish_spork/src/bookish_spork_transport.erl"},
{line,85}]},
{bookish_spork_acceptor,accept,2,
[{file,
"/Users/linus/Code/bookish_spork/src/bookish_spork_acceptor.erl"},
{line,64}]},
{bookish_spork_acceptor,handle_cast,2,
[{file,
"/Users/linus/Code/bookish_spork/src/bookish_spork_acceptor.erl"},
{line,48}]},
If postgresql is running, bookish_spork
won't work silently, because port is already in use.
Hi.
I see from the code that it seems to be possible to stub several requests at once, i.e. when I need to tests multiple pages response. This would require stubbing request according to the request query I think. For example response contains some next
marker or url for the next page.
So I'd need to stub it like that
:bookish_spork.stub_request("/", 200, Poison.encode!(first_page_data))
:bookish_spork.stub_request("/?next_page=2", 200, Poison.encode!(second_page_data))
Would it be possible?
If yes, could you please add an example to your example apps how one could use it?
Hi, I really like this library and I've been using it for testing my things.
However, I recently ran into a rare issue I'd like to write a test for but I'm not sure how.
I need to test requests that time out. Do you have an idea on how I would do that with this library?
Lines 14 to 18 in 2cbe753
Hello.
I recently started to use bookish_spork
instead of bypass
, because bookish_spork
does not have cowboy
dependency and I wanted to use gun
. However I got some problems testing HTTP requests with gun
. The problem is that when I stub multiple requests, gun
fails to get second response or gets it after ~5 seconds timeout. In order to demonstrate it I've created a test repo for you https://github.com/gaynetdinov/gun_with_spork.
There is HttpClient
module which is a simple wrapper around gun
(because gun
is quite verbose) and there is simple test file with several test cases there.
gun
works ok when I create a separate connection per request, but if I want to reuse already established connection (see https://github.com/gaynetdinov/gun_with_spork/blob/master/test/gun_with_spork_test.exs#L28) I either get second response after 4-5 seconds or get the following error
$ mix test
[error] Process #PID<0.210.0> raised an exception
** (CaseClauseError) no case clause matching: {:error, :closed}
(bookish_spork) /Users/damir/projects/gun_with_spork/deps/bookish_spork/src/bookish_spork_server.erl:95: :bookish_spork_server.receive_request/2
(bookish_spork) /Users/damir/projects/gun_with_spork/deps/bookish_spork/src/bookish_spork_server.erl:82: anonymous fn/3 in :bookish_spork_server.accept/3
Do you know why this is happening? Or maybe you could suggest some workarounds?
Thank you for your time!
Hello.
Sorry, it's me again. I just upgraded to 0.2.4 and got this error
Upgraded:
bookish_spork 0.1.1 => 0.2.4
* Updating bookish_spork (Hex package)
Error evaluating Rebar config script ./rebar.config.script:15: evaluation failed with reason error:undef and stacktrace [{rebar_git_resource,make_vsn,[[46]],[]},{erl_eval,do_apply,6,[{file,[101,114,108,95,101,118,97,108,46,101,114,108]},{line,670}]},{erl_eval,expr,5,[{file,[101,114,108,95,101,118,97,108,46,101,114,108]},{line,438}]},{erl_eval,exprs,5,[{file,[101,114,108,95,101,118,97,108,46,101,114,108]},{line,122}]},{file,eval_stream2,6,[{file,[102,105,108,101,46,101,114,108]},{line,1397}]},{file,script,2,[{file,[102,105,108,101,46,101,114,108]},{line,1097}]},{'Elixir.File','cd!',2,[{file,[108,105,98,47,102,105,108,101,46,101,120]},{line,1277}]},{'Elixir.Mix.Rebar',eval_script,2,[{file,[108,105,98,47,109,105,120,47,114,101,98,97,114,46,101,120]},{line,190}]}]
Any dependencies defined in the script won't be available unless you add them to your Mix project
I believe that this commit 6a83e53 introduced it, because there is no such error with version 0.2.3.
It can be useful to have some connection id. If connection: keep-alive
then we could see the same connection id. And on the other hand when connection: close
we could see that connection id has changed
setup_all do
{:ok, []} = HTTPoison.start()
{:ok, _} = :bookish_spork.start_server()
{:ok, %{}}
end
test "just test" do
:bookish_spork.stub_request([200, %{}, ""])
api_endpoint = "http://localhost:32002/api"
payload = "{\"destinations\":[],\"whatsApp\":{\"text\":\"Do Androids Dream of Electric Sheep?\"}}"
get_headers = [
{"Authorization", "Basic Q1wewewewewewewewewewg=="},
{"accept", "application/json"},
{"Content-Type", "application/json"}
]
http_options = []
result = HTTPoison.post(api_endpoint(), payload, get_headers(), http_options())
{:ok, request} = :bookish_spork.capture_request()
end
** (MatchError) no match of right hand side value: {:error, :no_request}
code: {:ok, request} = :bookish_spork.capture_request()
No error
Erlang/OTP 20
elixir (1.8.1)
"bookish_spork": {:hex, :bookish_spork, "0.3.3", "975c93911dfef1e581b477348739ab388eff0e2edf329bc7668e7ecd10568af8", [:rebar3], [], "hexpm"},
Thus we should implement stub/capture logic as a default handler. I suggest to look at elli because it's interface is neat
Format with erl_tidy and add CI check if possible
Line 14 in 48bf1ae
It looks like travis doensn't work well. We can see random red builds without any reasonable errors
It will allow to
-ifdef(OTP_RELEASE).
code_change/3
bookish_spork_acceptor
In some cases, order can not be guaranteed, because everything happens asynchronously
mix deps.update bookish_spork
Upgraded:
bookish_spork 0.3.0 => 0.3.1
* Updating bookish_spork (Hex package)
Error evaluating Rebar config script ./rebar.config.script:30: evaluation failed with reason error:{badmatch,{error,enoent}} and stacktrace [{erl_eval,expr,5,[{file,[101,114,108,95,101,118,97,108,46,101,114,108]},{line,453}]},{erl_eval,exprs,5,[{file,[101,114,108,95,101,118,97,108,46,101,114,108]},{line,126}]},{file,eval_stream2,6,[{file,[102,105,108,101,46,101,114,108]},{line,1393}]},{file,script,2,[{file,[102,105,108,101,46,101,114,108]},{line,1090}]},{'Elixir.File','cd!',2,[{file,[108,105,98,47,102,105,108,101,46,101,120]},{line,1443}]},{'Elixir.Mix.Rebar',eval_script,2,[{file,[108,105,98,47,109,105,120,47,114,101,98,97,114,46,101,120]},{line,195}]},{'Elixir.File','cd!',2,[{file,[108,105,98,47,102,105,108,101,46,101,120]},{line,1443}]}]
Any dependencies defined in the script won't be available unless you add them to your Mix project
with rebar3
===> Error evaluating configuration script at "/bookish_spork/examples/chuck/_build/test/lib/bookish_spork/rebar.config.script":
{error,{30,file,
{error,{badmatch,{error,enoent}},
[{erl_eval,expr,5,[{file,"erl_eval.erl"},{line,453}]},
{erl_eval,exprs,5,[{file,"erl_eval.erl"},{line,126}]},
{file,eval_stream2,6,[{file,"file.erl"},{line,1393}]},
{file,script,2,[{file,"file.erl"},{line,1090}]},
{rebar_config,consult_and_eval,2,
[{file,"/opt/rebar3-3.7.5/src/rebar_config.erl"},
{line,287}]},
{rebar_config,consult_file_,1,
[{file,"/opt/rebar3-3.7.5/src/rebar_config.erl"},
{line,230}]},
{rebar_config,consult_file,1,
[{file,"/opt/rebar3-3.7.5/src/rebar_config.erl"},
{line,212}]}]}}}
===> Uncaught error in rebar_core. Run with DEBUG=1 to see stacktrace or consult rebar3.crashdump
Erlang/OTP 21 [erts-10.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
IEx 1.7.4 (compiled with Erlang/OTP 21)
Rebar3 report
version 3.7.5
generated at 2019-01-27T09:25:26+00:00
=================
Please submit this along with your issue at https://github.com/erlang/rebar3/issues (and feel free to edit out private information, if any)
-----------------
Task:
Entered as:
-----------------
Operating System: x86_64-apple-darwin17.7.0
ERTS: Erlang/OTP 21 [erts-10.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Root Directory: /Users/xoma/kerl/21.2
Library directory: /Users/xoma/kerl/21.2/lib
-----------------
Loaded Applications:
bbmustache: 1.6.0
certifi: 2.3.1
cf: 0.2.2
common_test: 1.16.1
compiler: 7.3
crypto: 4.4
cth_readable: 1.4.2
dialyzer: 3.3.1
edoc: 0.9.4
erlware_commons: 1.3.0
eunit: 2.3.7
eunit_formatters: 0.5.0
getopt: 1.0.1
hex_core: 0.2.0
hipe: 3.18.2
inets: 7.0.3
kernel: 6.2
providers: 1.7.0
public_key: 1.6.4
relx: 3.27.0
sasl: 3.3
snmp: 5.2.12
ssl_verify_fun: 1.1.3
stdlib: 3.7
syntax_tools: 2.1.6
tools: 3.0.2
-----------------
Escript path: /bin/rebar3
Providers:
app_discovery as auto clean compile compile config cover ct cut deps dialyzer do docs edoc escriptize eunit get-deps help info install install_deps key list lock new owner path pkgs publish release relup report repos search shell state tar tree unlock update upgrade upgrade upgrade user version xref
bookish_spork:stub_request(fun(Request) ->
case bookish_spork_request:uri(Request) of
"/bookish/spork" ->
[200, [], <<"Hello">>];
"/admin/sporks" ->
[403, [], <<"It is not possible here">>]
end
end)
bookish_spork
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.