diff --git a/Makefile b/Makefile index 92efd8e..646dc62 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ LOCAL_DEPS := tools compiler syntax_tools common_test inets test_server dialyzer TEST_ERLC_OPTS += +debug_info CT_OPTS = -cover test/cowboy_swagger.coverspec -erl_args -config ${CONFIG} -SHELL_OPTS = -s sync +SHELL_OPTS = -s sync -config ${CONFIG} quicktests: app @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" diff --git a/src/cowboy_swagger_handler.erl b/src/cowboy_swagger_handler.erl index ce99b2d..f723ef4 100644 --- a/src/cowboy_swagger_handler.erl +++ b/src/cowboy_swagger_handler.erl @@ -14,25 +14,27 @@ %% Trails -behaviour(trails_handler). --export([trails/0]). +-export([trails/0, trails/1]). -type state() :: #{}. +-type route_match() :: '_' | iodata(). +-type options() :: #{server => ranch:ref(), host => route_match()}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Cowboy Callbacks %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @hidden --spec init({atom(), atom()}, cowboy_req:req(), state()) -> +-spec init({atom(), atom()}, cowboy_req:req(), options()) -> {upgrade, protocol, cowboy_rest}. init(_Transport, _Req, _Opts) -> {upgrade, protocol, cowboy_rest}. %% @hidden --spec rest_init(cowboy_req:req(), state()) -> - {ok, cowboy_req:req(), term()}. -rest_init(Req, _Opts) -> - {ok, Req, #{}}. +-spec rest_init(cowboy_req:req(), options()) -> + {ok, cowboy_req:req(), options()}. +rest_init(Req, Opts) -> + {ok, Req, Opts}. %% @hidden -spec content_types_provided(cowboy_req:req(), state()) -> @@ -46,7 +48,9 @@ content_types_provided(Req, State) -> %% @hidden handle_get(Req, State) -> - Trails = trails:all(), + Server = maps:get(server, State, '_'), + HostMatch = maps:get(host, State, '_'), + Trails = trails:all(Server, HostMatch), {cowboy_swagger:to_json(Trails), Req, State}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -54,10 +58,13 @@ handle_get(Req, State) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @hidden -%% @doc Implementets `trails_handler:trails/0' callback. This function returns +%% @doc Implements `trails_handler:trails/0' callback. This function returns %% trails routes for both: static content (Swagger-UI) and this handler %% that returns the `swagger.json'. -trails() -> +-spec trails() -> trails:trails(). +trails() -> trails(#{}). +-spec trails(Options::options()) -> trails:trails(). +trails(Options) -> StaticFiles = case application:get_env(cowboy_swagger, static_files) of {ok, Val} -> Val; @@ -81,7 +88,7 @@ trails() -> } }, Handler = trails:trail( - "/api-docs/swagger.json", cowboy_swagger_handler, [], MD), + "/api-docs/swagger.json", cowboy_swagger_handler, Options, MD), [Static1, Handler, Static2]. %% @private @@ -89,10 +96,9 @@ trails() -> cowboy_swagger_priv() -> case code:priv_dir(cowboy_swagger) of {error, bad_name} -> - filename:join( - [ filename:dirname(code:which(cowboy_swagger_handler)) - , ".." - , "priv" - ]); + case code:which(cowboy_swagger_handler) of + cover_compiled -> "../../priv"; % required for tests to work + BeamPath -> filename:join([filename:dirname(BeamPath) , ".." , "priv"]) + end; Path -> Path end. diff --git a/test/cowboy_swagger_handler_SUITE.erl b/test/cowboy_swagger_handler_SUITE.erl index 32d5957..c6a42a9 100644 --- a/test/cowboy_swagger_handler_SUITE.erl +++ b/test/cowboy_swagger_handler_SUITE.erl @@ -4,10 +4,13 @@ -export([ all/0 , init_per_suite/1 , end_per_suite/1 + , init_per_testcase/2 + , end_per_testcase/2 ]). %% Test cases --export([handler_test/1]). +-export([ handler_test/1 + , multiple_hosts_test/1]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Common test @@ -22,7 +25,6 @@ all() -> ) -> cowboy_swagger_test_utils:config(). init_per_suite(Config) -> {ok, _} = shotgun:start(), - {ok, _} = example:start(), Config. -spec end_per_suite( @@ -30,9 +32,37 @@ init_per_suite(Config) -> ) -> cowboy_swagger_test_utils:config(). end_per_suite(Config) -> _ = shotgun:stop(), + Config. + +-spec init_per_testcase(TestCase::atom(), + Config::cowboy_swagger_test_utils:config()) -> + cowboy_swagger_test_utils:config(). +init_per_testcase(handler_test, Config) -> + {ok, _} = example:start(), + Config; +init_per_testcase(multiple_hosts_test, Config) -> + {ok, _} = multiple_hosts_servers_example:start(), + Config. + +-spec end_per_testcase(TestCase::atom(), + Config::cowboy_swagger_test_utils:config()) -> + cowboy_swagger_test_utils:config(). +end_per_testcase(handler_test, Config) -> _ = example:stop(), + ok = cleanup(), + Config; +end_per_testcase(multiple_hosts_test, Config) -> + _ = multiple_hosts_servers_example:stop(), + ok = cleanup(), Config. +%% @private +-spec cleanup() -> ok. +cleanup() -> + _ = application:stop(cowboy_swagger), + _ = application:stop(trails), + ok. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Test Cases %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -73,5 +103,55 @@ handler_test(_Config) -> ct:comment("GET /api-docs/unknown-file.ext should return 404 NOT FOUND"), #{status_code := 404} = cowboy_swagger_test_utils:api_call(get, "/api-docs/unknown-file.ext"), + {comment, ""}. +-spec multiple_hosts_test(_Config::cowboy_swagger_test_utils:config()) -> + {atom(), string()}. +multiple_hosts_test(_Config) -> + %% api1 - host1 + Trails11 = trails:trails(example_echo_handler), + ExpectedPaths11 = get_expected_paths(Trails11), + %% GET swagger.json spec from localhost:8383 + ct:comment("GET /api-docs/swagger.json should return 200 OK"), + #{status_code := 200, body := Body11} = + cowboy_swagger_test_utils:api_call(get, + "/api-docs/swagger.json", + "localhost", + 8383), + #{<<"swagger">> := <<"2.0">>, + <<"info">> := #{<<"title">> := <<"Example API">>}, + <<"paths">> := ExpectedPaths11} = cowboy_swagger:dec_json(Body11), + %% api1 - host2 + Trails12 = trails:trails(host1_handler), + ExpectedPaths12 = get_expected_paths(Trails12), + %% GET swagger.json spec from 127.0.0.1:8383 + ct:comment("GET /api-docs/swagger.json should return 200 OK"), + #{status_code := 200, body := Body12} = + cowboy_swagger_test_utils:api_call(get, + "/api-docs/swagger.json", + "127.0.0.1", + 8383), + #{<<"swagger">> := <<"2.0">>, + <<"info">> := #{<<"title">> := <<"Example API">>}, + <<"paths">> := ExpectedPaths12} = cowboy_swagger:dec_json(Body12), + %% api2 - host1 + Trails21 = trails:trails([host1_handler, example_echo_handler]), + ExpectedPaths21 = get_expected_paths(Trails21), + %% GET swagger.json spec from localhost:8282 + ct:comment("GET /api-docs/swagger.json should return 200 OK"), + #{status_code := 200, body := Body21} = + cowboy_swagger_test_utils:api_call(get, + "/api-docs/swagger.json", + "localhost", + 8282), + #{<<"swagger">> := <<"2.0">>, + <<"info">> := #{<<"title">> := <<"Example API">>}, + <<"paths">> := ExpectedPaths21} = cowboy_swagger:dec_json(Body21), {comment, ""}. + +%% @private +-spec get_expected_paths(Trails::trails:trails()) -> jiffy:json_value(). +get_expected_paths(Trails) -> + SanitizeTrails = cowboy_swagger:filter_cowboy_swagger_handler(Trails), + cowboy_swagger:dec_json( + cowboy_swagger:enc_json(cowboy_swagger:swagger_paths(SanitizeTrails))). diff --git a/test/cowboy_swagger_test_utils.erl b/test/cowboy_swagger_test_utils.erl index 46749f1..7bda117 100644 --- a/test/cowboy_swagger_test_utils.erl +++ b/test/cowboy_swagger_test_utils.erl @@ -5,7 +5,6 @@ , end_per_suite/1 ]). -export([ api_call/2 - , api_call/3 , api_call/4 ]). @@ -28,17 +27,13 @@ end_per_suite(Config) -> -spec api_call(atom(), string()) -> #{}. api_call(Method, Uri) -> - api_call(Method, Uri, #{}). + api_call(Method, Uri, "localhost", 8080). --spec api_call(atom(), string(), #{}) -> #{}. -api_call(Method, Uri, Headers) -> - api_call(Method, Uri, Headers, []). - --spec api_call(atom(), string(), #{}, iodata()) -> #{}. -api_call(Method, Uri, Headers, Body) -> - Port = application:get_env(example, http_port, 8080), - {ok, Pid} = shotgun:open("localhost", Port), +-spec api_call(atom(), string(), string(), integer()) -> #{}. +api_call(Method, Uri, HostMatch, Port) -> + {ok, Pid} = shotgun:open(HostMatch, Port), try + Headers = #{}, Body = [], {ok, Response} = shotgun:request(Pid, Method, Uri, Headers, Body, #{}), Response after diff --git a/test/host1_handler.erl b/test/host1_handler.erl new file mode 100644 index 0000000..c22e8d1 --- /dev/null +++ b/test/host1_handler.erl @@ -0,0 +1,44 @@ +-module(host1_handler). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([ + {example_default, + [ + init/3, + rest_init/2, + content_types_accepted/2, + content_types_provided/2, + resource_exists/2 + ]} + ]). + +-export([ allowed_methods/2 + , handle_get/2]). + +%trails +-behaviour(trails_handler). +-export([trails/0]). + +trails() -> + Metadata = + #{get => + #{tags => ["whoami"], + description => "Get hostname", + produces => ["text/plain"] + } + }, + [trails:trail( + "/whoami", + host1_handler, + #{}, + Metadata)]. + +%% cowboy +allowed_methods(Req, State) -> + {[<<"GET">>], Req, State}. + +%% internal +handle_get(Req, State) -> + {Host, Req1} = cowboy_req:host(Req), + Body = <<"I am ", Host/binary>>, + {Body, Req1, State}. diff --git a/test/multiple_hosts_servers_example.app b/test/multiple_hosts_servers_example.app new file mode 100644 index 0000000..249e78f --- /dev/null +++ b/test/multiple_hosts_servers_example.app @@ -0,0 +1,18 @@ +{application, multiple_hosts_servers_example, + [ + {description, "Cowboy Swagger Complex Example."}, + {vsn, "0.1"}, + {applications, + [ kernel + , stdlib + , sasl + + , cowboy + , trails + , cowboy_swagger + ]}, + {modules, []}, + {mod, {multiple_hosts_servers_example, []}}, + {start_phases, [{start_multiple_hosts_servers_example_http, []}]} + ] +}. diff --git a/test/multiple_hosts_servers_example.erl b/test/multiple_hosts_servers_example.erl new file mode 100644 index 0000000..c76b5ca --- /dev/null +++ b/test/multiple_hosts_servers_example.erl @@ -0,0 +1,66 @@ +-module(multiple_hosts_servers_example). + +-export([start/0]). +-export([start/2]). +-export([stop/0]). +-export([stop/1]). +-export([start_phase/3]). + +%% application +%% @doc Starts the application +start() -> + application:ensure_all_started(multiple_hosts_servers_example). + +%% @doc Stops the application +stop() -> + application:stop(multiple_hosts_servers_example). + +%% behaviour +%% @private +start(_StartType, _StartArgs) -> + _ = application:stop(lager), + ok = application:stop(sasl), + {ok, _} = application:ensure_all_started(sasl), + {ok, self()}. + +%% @private +stop(_State) -> + ok = cowboy:stop_listener(multiple_hosts_servers_http). + +-spec start_phase(atom(), application:start_type(), []) -> ok | {error, term()}. +start_phase(start_multiple_hosts_servers_example_http, _StartType, []) -> + %% Host1 + {ok, #{hosts := [HostMatch11, HostMatch12], port := Port1}} = + application:get_env(multiple_hosts_servers_example, api1), + {ok, #{hosts := ['_'], port := Port2}} = + application:get_env(multiple_hosts_servers_example, api2), + {ok, ListenerCount} = + application:get_env(multiple_hosts_servers_example, http_listener_count), + + Trails11 = + trails:trails(example_echo_handler) ++ + cowboy_swagger_handler:trails(#{server => api1, host => HostMatch11}), + Trails12 = + trails:trails(host1_handler) ++ + cowboy_swagger_handler:trails(#{server => api1, host => HostMatch12}), + Routes1 = [{HostMatch11, Trails11}, {HostMatch12, Trails12}], + + trails:store(api1, Routes1), + Dispatch1 = trails:compile(Routes1), + {ok, _} = start_cowboy(api1, ListenerCount, Dispatch1, Port1), + + Trails21 = + trails:trails([host1_handler, example_echo_handler]) ++ + cowboy_swagger_handler:trails(#{server => api2}), + + trails:store(api2, Trails21), + Dispatch2 = trails:single_host_compile(Trails21), + {ok, _} = start_cowboy(api2, ListenerCount, Dispatch2, Port2), + ok. + +%% @private +start_cowboy(Server, ListenerCount, Dispatch, Port) -> + RanchOptions = [{port, Port}], + CowboyOptions = + [{env, [{dispatch, Dispatch}]}, {compress, true}, {timeout, 12000}], + cowboy:start_http(Server, ListenerCount, RanchOptions, CowboyOptions). diff --git a/test/test.config b/test/test.config index ed50bea..f3d3c89 100644 --- a/test/test.config +++ b/test/test.config @@ -6,13 +6,20 @@ ] }, + {multiple_hosts_servers_example, + [ + {http_listener_count, 10}, + {api1, #{hosts => ["localhost", "127.0.0.1"], port => 8383}}, + {api2, #{hosts => ['_'], port => 8282}} + ] + }, + {cowboy_swagger, [ - {static_files, "../../priv/swagger"}, {global_spec, #{swagger => "2.0", info => #{title => "Example API"}, - basePath => "/api-docs" + basePath => "" } } ]