diff --git a/README.md b/README.md index 1433898..fc9e621 100644 --- a/README.md +++ b/README.md @@ -212,11 +212,29 @@ We can use this token for an entire hour, after that we will receive something l ## Reconnection -If network goes down or something unexpected happens the `gun` connection with APNs will go down. In that case `apns4erl` will send a message `{reconnecting, ServerPid}` to the client process, that means `apns4erl` lost the connection and it is trying to reconnect. Once the connection has been recover a `{connection_up, ServerPid}` message will be send. - +If something unexpected happens and the `chatterbox` connection with APNs crashes `apns4erl` will send a message `{reconnecting, ServerPid}` to the client process, that means `apns4erl` lost the connection and it is trying to reconnect. Once the connection has been recover a `{connection_up, ServerPid}` message will be send. We implemented an *Exponential Backoff* strategy. We can set the *ceiling* time adding the `backoff_ceiling` variable on the `config` file. By default it is set to 10 (seconds). +## Timeout + +When we call `apns:push_notification/3,4` or `apns:push_notification_token/4,5` we could get a `timeout` that could be caused if the network went down. Here is the `timeout` format: + +```erlang +{timeout, stream_id()} +``` +where that `stream_id()` is an identifier for the notification. + + +Getting a `timeout` doesn't mean your notification to APNs is lost. If `apns4erl` connects to network again it will try to send your notification, in that case `apns4erl` will send back a message to the client with the format: + +```erlang +{apns_response, ServerPid, StreamID, Response} +``` +where that StreamId should match with the `stream_id` we got on the `timeout` tuple. + +You should check your client inbox after a timeout but it is not guaranteed your message was send successfully. + ## Close connections Apple recommends us to keep our connections open and avoid opening and closing very often. You can check the [Best Practices for Managing Connections](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) section. diff --git a/rebar.config b/rebar.config index 8b0df58..d55b11a 100644 --- a/rebar.config +++ b/rebar.config @@ -23,7 +23,7 @@ %% == Dependencies == {deps, [ - {gun, {git, "https://github.com/ninenines/gun.git", {ref, "bc733a2ca5f7d07f997ad6edf184f775b23434aa"}}}, + {chatterbox, "0.4.2"}, {jsx, "2.8.1"}, {base64url, "0.0.1"} ]}. diff --git a/rebar.lock b/rebar.lock index b67f9cd..dae26d2 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,20 +1,16 @@ {"1.1.0", [{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0}, - {<<"cowlib">>, - {git,"https://github.com/ninenines/cowlib", - {ref,"07cde7c6def6eeed0174b270229e7d5175673d87"}}, - 1}, - {<<"gun">>, - {git,"https://github.com/ninenines/gun.git", - {ref,"bc733a2ca5f7d07f997ad6edf184f775b23434aa"}}, - 0}, + {<<"chatterbox">>,{pkg,<<"chatterbox">>,<<"0.4.2">>},0}, + {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},2}, + {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.2.3">>},1}, {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.1">>},0}, - {<<"ranch">>, - {git,"https://github.com/ninenines/ranch", - {ref,"a004ad710eddd0c21aaccc30d5633a76b06164b5"}}, - 1}]}. + {<<"lager">>,{pkg,<<"lager">>,<<"3.2.4">>},1}]}. [ {pkg_hash,[ {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, - {<<"jsx">>, <<"1453B4EB3615ACB3E2CD0A105D27E6761E2ED2E501AC0B390F5BBEC497669846">>}]} + {<<"chatterbox">>, <<"BA68296FA79F0CA31139713C688C350A26C8844E1244F1736D7845C1C72E5F87">>}, + {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, + {<<"hpack">>, <<"17670F83FF984AE6CD74B1C456EDDE906D27FF013740EE4D9EFAA4F1BF999633">>}, + {<<"jsx">>, <<"1453B4EB3615ACB3E2CD0A105D27E6761E2ED2E501AC0B390F5BBEC497669846">>}, + {<<"lager">>, <<"A6DEB74DAE7927F46BD13255268308EF03EB206EC784A94EAF7C1C0F3B811615">>}]} ]. diff --git a/src/apns.app.src b/src/apns.app.src index 2e29c05..59ee771 100644 --- a/src/apns.app.src +++ b/src/apns.app.src @@ -7,7 +7,7 @@ kernel, stdlib, jsx, - gun, + chatterbox, base64url ]}, {modules, []}, diff --git a/src/apns.erl b/src/apns.erl index 2d1319e..a873eda 100644 --- a/src/apns.erl +++ b/src/apns.erl @@ -40,14 +40,16 @@ , response/0 , token/0 , headers/0 + , stream_id/0 ]). --type json() :: #{binary() => binary() | json()}. +-type json() :: #{binary() | atom() => binary() | json()}. -type device_id() :: binary(). +-type stream_id() :: integer(). -type response() :: { integer() % HTTP2 Code , [term()] % Response Headers , [term()] | no_body % Response Body - } | timeout. + } | {timeout, stream_id()}. -type token() :: binary(). -type headers() :: #{ apns_id => binary() , apns_expiration => binary() @@ -84,9 +86,7 @@ connect(Type, ConnectionName) -> %% @doc Connects to APNs service -spec connect(apns_connection:connection()) -> {ok, pid()} | {error, timeout}. connect(Connection) -> - {ok, _} = apns_sup:create_connection(Connection), - Server = whereis(apns_connection:name(Connection)), - apns_connection:wait_apns_connection_up(Server). + apns_sup:create_connection(Connection). %% @doc Closes the connection with APNs service. -spec close_connection(apns_connection:name()) -> ok. diff --git a/src/apns_connection.erl b/src/apns_connection.erl index dea8d06..778d90f 100644 --- a/src/apns_connection.erl +++ b/src/apns_connection.erl @@ -30,11 +30,10 @@ , certfile/1 , keyfile/1 , type/1 - , gun_connection/1 + , http2_connection/1 , close_connection/1 , push_notification/4 , push_notification/5 - , wait_apns_connection_up/1 ]). %% gen_server callbacks @@ -69,11 +68,11 @@ , type := type() }. --type state() :: #{ connection := connection() - , gun_connection := pid() - , client := pid() - , backoff := non_neg_integer() - , backoff_ceiling := non_neg_integer() +-type state() :: #{ connection := connection() + , http2_connection := pid() + , client := pid() + , backoff := non_neg_integer() + , backoff_ceiling := non_neg_integer() }. %%%=================================================================== @@ -121,10 +120,10 @@ default_connection(token, ConnectionName) -> close_connection(ConnectionName) -> gen_server:cast(ConnectionName, stop). -%% @doc Returns the gun's connection PID. This function is only used in tests. --spec gun_connection(name()) -> pid(). -gun_connection(ConnectionName) -> - gen_server:call(ConnectionName, gun_connection). +%% @doc Returns the http2's connection PID. This function is only used in tests. +-spec http2_connection(name()) -> pid(). +http2_connection(ConnectionName) -> + gen_server:call(ConnectionName, http2_connection). %% @doc Pushes notification to certificate APNs connection. -spec push_notification( name() @@ -132,11 +131,9 @@ gun_connection(ConnectionName) -> , notification() , apns:headers()) -> apns:response(). push_notification(ConnectionName, DeviceId, Notification, Headers) -> - gen_server:call(ConnectionName, { push_notification - , DeviceId - , Notification - , Headers - }). + {Timeout, StreamId} = + gen_server:call(ConnectionName, {push_notification, DeviceId, Notification, Headers}), + wait_response(ConnectionName, Timeout, StreamId). %% @doc Pushes notification to certificate APNs connection. -spec push_notification( name() @@ -145,55 +142,45 @@ push_notification(ConnectionName, DeviceId, Notification, Headers) -> , notification() , apns:headers()) -> apns:response(). push_notification(ConnectionName, Token, DeviceId, Notification, Headers) -> - gen_server:call(ConnectionName, { push_notification - , Token - , DeviceId - , Notification - , Headers - }). - -%% @doc Waits until receive the `connection_up` message --spec wait_apns_connection_up(pid()) -> {ok, pid()} | {error, timeout}. -wait_apns_connection_up(Server) -> - receive - {connection_up, Server} -> {ok, Server}; - {timeout, Server} -> {error, timeout} - end. + {Timeout, StreamId} = + gen_server:call(ConnectionName, {push_notification, Token, DeviceId, Notification, Headers}), + wait_response(ConnectionName, Timeout, StreamId). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== --spec init({connection(), pid()}) -> {ok, State :: state(), timeout()}. +-spec init({connection(), pid()}) -> {ok, State :: state()}. init({Connection, Client}) -> - GunConnectionPid = open_gun_connection(Connection), + process_flag(trap_exit, true), + ConnectionPid = open_http2_connection(Connection), - {ok, #{ connection => Connection - , gun_connection => GunConnectionPid - , client => Client - , backoff => 1 - , backoff_ceiling => application:get_env(apns, backoff_ceiling, 10) - }, 0}. + {ok, #{ connection => Connection + , http2_connection => ConnectionPid + , client => Client + , backoff => 1 + , backoff_ceiling => application:get_env(apns, backoff_ceiling, 10) + }}. -spec handle_call( Request :: term(), From :: {pid(), term()}, State) -> {reply, ok, State}. -handle_call(gun_connection, _From, #{gun_connection := GunConn} = State) -> - {reply, GunConn, State}; +handle_call(http2_connection, _From, #{http2_connection := HTTP2Conn} = State) -> + {reply, HTTP2Conn, State}; handle_call( {push_notification, DeviceId, Notification, Headers} , _From , State) -> - #{connection := Connection, gun_connection := GunConn} = State, + #{connection := Connection, http2_connection := HTTP2Conn} = State, #{timeout := Timeout} = Connection, - Response = push(GunConn, DeviceId, Headers, Notification, Timeout), - {reply, Response, State}; + StreamId = push(HTTP2Conn, DeviceId, Headers, Notification, Connection), + {reply, {Timeout, StreamId}, State}; handle_call( {push_notification, Token, DeviceId, Notification, HeadersMap} , _From , State) -> - #{connection := Connection, gun_connection := GunConn} = State, - #{timeout := Timeout} = Connection, + #{connection := Connection, http2_connection := HTTP2Conn} = State, Headers = add_authorization_header(HeadersMap, Token), - Response = push(GunConn, DeviceId, Headers, Notification, Timeout), - {reply, Response, State}; + #{timeout := Timeout} = Connection, + StreamId = push(HTTP2Conn, DeviceId, Headers, Notification, Connection), + {reply, {Timeout, StreamId}, State}; handle_call(_Request, _From, State) -> {reply, ok, State}. @@ -205,13 +192,13 @@ handle_cast(_Request, State) -> {noreply, State}. -spec handle_info(Info :: timeout() | term(), State) -> {noreply, State}. -handle_info( {gun_down, GunConn, http2, closed, _, _} - , #{ gun_connection := GunConn - , client := Client - , backoff := Backoff - , backoff_ceiling := Ceiling +handle_info( {'EXIT', HTTP2Conn, _} + , #{ http2_connection := HTTP2Conn + , client := Client + , backoff := Backoff + , backoff_ceiling := Ceiling } = State) -> - ok = gun:close(GunConn), + ok = h2_client:stop(HTTP2Conn), Client ! {reconnecting, self()}, Sleep = backoff(Backoff, Ceiling) * 1000, % seconds to wait before reconnect {ok, _} = timer:send_after(Sleep, reconnect), @@ -219,33 +206,16 @@ handle_info( {gun_down, GunConn, http2, closed, _, _} handle_info(reconnect, State) -> #{ connection := Connection , client := Client - , backoff := Backoff - , backoff_ceiling := Ceiling } = State, - GunConn = open_gun_connection(Connection), - #{timeout := Timeout} = Connection, - case gun:await_up(GunConn, Timeout) of - {ok, http2} -> - Client ! {connection_up, self()}, - {noreply, State#{ gun_connection => GunConn - , backoff => 1}}; - {error, timeout} -> - ok = gun:close(GunConn), - Sleep = backoff(Backoff, Ceiling) * 1000, % seconds to wait - {ok, _} = timer:send_after(Sleep, reconnect), - {noreply, State#{backoff => Backoff + 1}} - end; -handle_info(timeout, #{connection := Connection, gun_connection := GunConn, - client := Client} = State) -> - #{timeout := Timeout} = Connection, - case gun:await_up(GunConn, Timeout) of - {ok, http2} -> - Client ! {connection_up, self()}, - {noreply, State}; - {error, timeout} -> - Client ! {timeout, self()}, - {stop, timeout, State} - end; + HTTP2Conn = open_http2_connection(Connection), + Client ! {connection_up, self()}, + {noreply, State#{http2_connection => HTTP2Conn , backoff => 1}}; +handle_info({'END_STREAM', StreamId}, #{http2_connection := HTTP2Conn, client := Client} = State) -> + {ok, {ResponseHeaders, ResponseBody}} = h2_client:get_response(HTTP2Conn, StreamId), + {Status, ResponseHeaders2} = normalize_response(ResponseHeaders), + ResponseBody2 = normalize_response_body(ResponseBody), + Client ! {apns_response, self(), StreamId, {Status, ResponseHeaders2, ResponseBody2}}, + {noreply, State}; handle_info(_Info, State) -> {noreply, State}. @@ -294,10 +264,9 @@ type(#{type := Type}) -> %%% Internal Functions %%%=================================================================== --spec open_gun_connection(connection()) -> GunConnectionPid :: pid(). -open_gun_connection(Connection) -> +-spec open_http2_connection(connection()) -> ConnectionPid :: pid(). +open_http2_connection(Connection) -> Host = host(Connection), - Port = port(Connection), TransportOpts = case type(Connection) of cert -> @@ -307,16 +276,11 @@ open_gun_connection(Connection) -> token -> [] end, + {ok, ConnectionPid} = h2_client:start_link(https, Host, TransportOpts), + ConnectionPid. - {ok, GunConnectionPid} = gun:open( Host - , Port - , #{ protocols => [http2] - , transport_opts => TransportOpts - }), - GunConnectionPid. - --spec get_headers(apns:headers()) -> list(). -get_headers(Headers) -> +-spec get_headers(binary(), apns:headers(), connection()) -> list(). +get_headers(DeviceId, Headers, Connection) -> List = [ {<<"apns-id">>, apns_id} , {<<"apns-expiration">>, apns_expiration} , {<<"apns-priority">>, apns_priority} @@ -330,7 +294,18 @@ get_headers(Headers) -> Value -> [{ActualHeader, Value}] end end, - lists:flatmap(F, List). + Headers2 = lists:flatmap(F, List), + lists:append(Headers2, mandatory_headers(DeviceId, Connection)). + +-spec mandatory_headers(binary(), connection()) -> list(). +mandatory_headers(DeviceId, #{apple_host := Host, apple_port := Port}) -> + Host2 = list_to_binary(Host), + Port2 = integer_to_binary(Port), + [ {<<":method">>, <<"POST">>} + , {<<":path">>, get_device_path(DeviceId)} + , {<<":scheme">>, <<"https">>} + , {<<":authority">>, <>} + ]. -spec get_device_path(apns:device_id()) -> binary(). get_device_path(DeviceId) -> @@ -340,21 +315,31 @@ get_device_path(DeviceId) -> add_authorization_header(Headers, Token) -> Headers#{apns_auth_token => <<"bearer ", Token/binary>>}. --spec push(pid(), apns:device_id(), apns:headers(), notification(), - integer()) -> - apns:response(). -push(GunConn, DeviceId, HeadersMap, Notification, Timeout) -> - Headers = get_headers(HeadersMap), - Path = get_device_path(DeviceId), - StreamRef = gun:post(GunConn, Path, Headers, Notification), - case gun:await(GunConn, StreamRef, Timeout) of - {response, fin, Status, ResponseHeaders} -> - {Status, ResponseHeaders, no_body}; - {response, nofin, Status, ResponseHeaders} -> - {ok, Body} = gun:await_body(GunConn, StreamRef, Timeout), - DecodedBody = jsx:decode(Body), - {Status, ResponseHeaders, DecodedBody}; - {error, timeout} -> timeout +-spec push(pid(), apns:device_id(), apns:headers(), notification(), connection()) -> + apns:stream_id(). +push(HTTP2Conn, DeviceId, HeadersMap, Notification, Connection) -> + Headers = get_headers(DeviceId, HeadersMap, Connection), + {ok, StreamID} = h2_client:send_request(HTTP2Conn, Headers, Notification), + StreamID. + +-spec normalize_response(list()) -> {integer(), list()}. +normalize_response(ResponseHeaders) -> + {<<":status">>, Status} = lists:keyfind(<<":status">>, 1, ResponseHeaders), + {binary_to_integer(Status), lists:keydelete(<<":status">>, 1, ResponseHeaders)}. + +-spec normalize_response_body(list()) -> list() | no_body. +normalize_response_body([]) -> + no_body; +normalize_response_body([ResponseBody]) -> + jsx:decode(ResponseBody). + +-spec wait_response(name(), integer(), integer()) -> apns:response(). +wait_response(ConnectionName, Timeout, StreamID) -> + Server = whereis(ConnectionName), + receive + {apns_response, Server, StreamID, Response} -> Response + after + Timeout -> {timeout, StreamID} end. -spec backoff(non_neg_integer(), non_neg_integer()) -> non_neg_integer(). diff --git a/test/apns_os_SUITE.erl b/test/apns_os_SUITE.erl new file mode 100644 index 0000000..4ee4d60 --- /dev/null +++ b/test/apns_os_SUITE.erl @@ -0,0 +1,40 @@ +-module(apns_os_SUITE). +-author("Felipe Ripoll "). + +-export([ all/0 + , init_per_suite/1 + , end_per_suite/1 + ]). + +-export([ cmd/1 + ]). + +-type config() :: [{atom(), term()}]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Common test +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec all() -> [atom()]. +all() -> [ cmd + ]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + ok = apns:start(), + Config. + +-spec end_per_suite(config()) -> config(). +end_per_suite(Config) -> + ok = apns:stop(), + Config. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Test Cases +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec cmd(config()) -> ok. +cmd(_Config) -> + {0, _} = apns_os:cmd("ls"), + {1, _Error} = apns_os:cmd("no existing command"), + ok. diff --git a/test/connection_SUITE.erl b/test/connection_SUITE.erl index 8476cd1..d8f79e6 100644 --- a/test/connection_SUITE.erl +++ b/test/connection_SUITE.erl @@ -8,9 +8,7 @@ -export([ default_connection/1 , connect/1 - , connect_timeout/1 - , gun_connection_lost/1 - , gun_connection_lost_timeout/1 + , http2_connection_lost/1 , push_notification/1 , push_notification_token/1 , push_notification_timeout/1 @@ -27,9 +25,7 @@ -spec all() -> [atom()]. all() -> [ default_connection , connect - , connect_timeout - , gun_connection_lost - , gun_connection_lost_timeout + , http2_connection_lost , push_notification , push_notification_token , push_notification_timeout @@ -79,8 +75,7 @@ default_connection(_Config) -> -spec connect(config()) -> ok. connect(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), + ok = mock_open_http2_connection(), ConnectionName = my_connection, {ok, ServerPid} = apns:connect(cert, ConnectionName), true = is_process_alive(ServerPid), @@ -89,73 +84,48 @@ connect(_Config) -> [_] = meck:unload(), ok. --spec connect_timeout(config()) -> ok. -connect_timeout(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({error, timeout}), - ConnectionName = my_connection, - {error, timeout} = apns:connect(token, ConnectionName), - ok = close_connection(ConnectionName), - [_] = meck:unload(), - ok. - --spec gun_connection_lost(config()) -> ok. -gun_connection_lost(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), +-spec http2_connection_lost(config()) -> ok. +http2_connection_lost(_Config) -> + ok = mock_open_http2_connection(), ConnectionName = my_connection2, {ok, ServerPid} = apns:connect(cert, ConnectionName), - GunPid = apns_connection:gun_connection(ConnectionName), - true = is_process_alive(GunPid), - GunPid ! {crash, ServerPid}, - ktn_task:wait_for(fun() -> is_process_alive(GunPid) end, false), + HTTP2Conn = apns_connection:http2_connection(ConnectionName), + true = is_process_alive(HTTP2Conn), + HTTP2Conn ! {crash, ServerPid}, + ktn_task:wait_for(fun() -> is_process_alive(HTTP2Conn) end, false), ktn_task:wait_for(fun() -> - apns_connection:gun_connection(ConnectionName) == GunPid + apns_connection:http2_connection(ConnectionName) == HTTP2Conn end, false), - GunPid2 = apns_connection:gun_connection(ConnectionName), - true = is_process_alive(GunPid2), - true = (GunPid =/= GunPid2), - ok = close_connection(ConnectionName), - ktn_task:wait_for(fun() -> is_process_alive(GunPid2) end, false), - [_] = meck:unload(), - ok. - --spec gun_connection_lost_timeout(config()) -> ok. -gun_connection_lost_timeout(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), - ConnectionName = my_connection, - % when backoff is grater than ceiling - ok = application:set_env(apns, backoff_ceiling, 2), - {ok, _ServerPid} = apns:connect(cert, ConnectionName), - GunPid = apns_connection:gun_connection(ConnectionName), + HTTP2Conn2 = apns_connection:http2_connection(ConnectionName), + true = is_process_alive(HTTP2Conn2), + true = (HTTP2Conn =/= HTTP2Conn2), - ok = mock_gun_await_up({error, timeout}), - ok = meck:expect(gun, close, fun(_) -> - ok - end), + % Repeat with ceiling 0, for testing coverage + ok = application:set_env(apns, backoff_ceiling, 0), - ConnectionName ! reconnect, + ConnectionName2 = my_connection3, + {ok, ServerPid2} = apns:connect(cert, ConnectionName2), + HTTP2Conn3 = apns_connection:http2_connection(ConnectionName2), + true = is_process_alive(HTTP2Conn3), + HTTP2Conn3 ! {crash, ServerPid2}, + ktn_task:wait_for(fun() -> is_process_alive(HTTP2Conn3) end, false), ktn_task:wait_for(fun() -> - apns_connection:gun_connection(ConnectionName) == GunPid - end, false), - - GunPid2 = apns_connection:gun_connection(ConnectionName), - - ktn_task:wait_for(fun() -> - apns_connection:gun_connection(ConnectionName) == GunPid2 + apns_connection:http2_connection(ConnectionName2) == HTTP2Conn3 end, false), + HTTP2Conn4 = apns_connection:http2_connection(ConnectionName2), + true = is_process_alive(HTTP2Conn4), + true = (HTTP2Conn3 =/= HTTP2Conn4), + ok = application:unset_env(apns, backoff_ceiling), ok = close_connection(ConnectionName), - ok = application:set_env(apns, backoff_ceiling, 10), + ktn_task:wait_for(fun() -> is_process_alive(HTTP2Conn2) end, false), [_] = meck:unload(), ok. -spec push_notification(config()) -> ok. push_notification(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), + ok = mock_open_http2_connection(), ConnectionName = my_connection, {ok, _ApnsPid} = apns:connect(cert, ConnectionName), Headers = #{ apns_id => <<"apnsid">> @@ -165,23 +135,22 @@ push_notification(_Config) -> }, Notification = #{<<"aps">> => #{<<"alert">> => <<"you have a message">>}}, DeviceId = <<"device_id">>, - ok = mock_gun_post(), + ok = mock_http2_post(), ResponseCode = 200, ResponseHeaders = [{<<"apns-id">>, <<"apnsid">>}], - ok = mock_gun_await({response, fin, ResponseCode, ResponseHeaders}), + ok = mock_http2_get_response(ResponseCode, ResponseHeaders, []), + {ResponseCode, ResponseHeaders, no_body} = apns:push_notification(ConnectionName, DeviceId, Notification, Headers), %% Now mock an error from APNs - [_] = meck:unload(), - ok = mock_gun_post(), ErrorCode = 400, ErrorHeaders = [{<<"apns-id">>, <<"apnsid2">>}], - ErrorBody = <<"{\"reason\":\"BadTopic\"}">>, - ok = mock_gun_await({response, nofin, ErrorCode, ErrorHeaders}), - ok = mock_gun_await_body(ErrorBody), + ErrorBody = [<<"{\"reason\":\"BadDeviceToken\"}">>], + + ok = mock_http2_get_response(ErrorCode, ErrorHeaders, ErrorBody), - {ErrorCode, ErrorHeaders, _DecodedErrorBody} = + {ErrorCode, ErrorHeaders, _ErrorBodyDecoded} = apns:push_notification(ConnectionName, DeviceId, Notification), ok = close_connection(ConnectionName), [_] = meck:unload(), @@ -189,8 +158,7 @@ push_notification(_Config) -> -spec push_notification_token(config()) -> ok. push_notification_token(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), + ok = mock_open_http2_connection(), ConnectionName = my_token_connection, {ok, _ApnsPid} = apns:connect(token, ConnectionName), Headers = #{ apns_id => <<"apnsid2">> @@ -200,14 +168,13 @@ push_notification_token(_Config) -> }, Notification = #{<<"aps">> => #{<<"alert">> => <<"more messages">>}}, DeviceId = <<"device_id2">>, - + ok = mock_http2_post(), ok = maybe_mock_apns_os(), Token = apns:generate_token(<<"TeamId">>, <<"KeyId">>), - ok = mock_gun_post(), ResponseCode = 200, ResponseHeaders = [{<<"apns-id">>, <<"apnsid2">>}], - ok = mock_gun_await({response, fin, ResponseCode, ResponseHeaders}), + ok = mock_http2_get_response(ResponseCode, ResponseHeaders, []), {ResponseCode, ResponseHeaders, no_body} = apns:push_notification_token( ConnectionName , Token @@ -232,14 +199,14 @@ push_notification_timeout(_Config) -> {ok, OriginalTimeout} = application:get_env(apns, timeout), ok = application:set_env(apns, timeout, 0), - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), - ok = mock_gun_post(), + ok = mock_open_http2_connection(), + ok = mock_http2_post(), + ok = mock_http2_get_response(200, [{<<"apns-id">>, <<"apnsid">>}], []), ConnectionName = my_connection, {ok, _ApnsPid} = apns:connect(cert, ConnectionName), Notification = #{<<"aps">> => #{<<"alert">> => <<"another message">>}}, DeviceId = <<"device_id">>, - timeout = apns:push_notification(ConnectionName, DeviceId, Notification), + {timeout, _} = apns:push_notification(ConnectionName, DeviceId, Notification), ok = close_connection(ConnectionName), [_] = meck:unload(), @@ -285,8 +252,7 @@ default_headers(_Config) -> -spec test_coverage(config()) -> ok. test_coverage(_Config) -> - ok = mock_gun_open(), - ok = mock_gun_await_up({ok, http2}), + ok = mock_open_http2_connection(), ConnectionName = my_connection, {ok, _ServerPid} = apns:connect(cert, ConnectionName), @@ -306,40 +272,29 @@ test_coverage(_Config) -> -spec test_function() -> ok. test_function() -> receive - normal -> ok; - {crash, Pid} -> Pid ! {gun_down, self(), http2, closed, [], []}; - _ -> test_function() + normal -> ok; + {crash, Pid} -> Pid ! {'EXIT', self(), no_reason}; + _ -> test_function() end. --spec mock_gun_open() -> ok. -mock_gun_open() -> - meck:expect(gun, open, fun(_, _, _) -> +-spec mock_open_http2_connection() -> ok. +mock_open_http2_connection() -> + meck:expect(h2_client, start_link, fun(https, _, _) -> % Return a Pid but nothing special with it {ok, spawn(fun test_function/0)} end). --spec mock_gun_post() -> ok. -mock_gun_post() -> - meck:expect(gun, post, fun(_, _, _, _) -> - make_ref() - end). - --spec mock_gun_await(term()) -> ok. -mock_gun_await(Result) -> - meck:expect(gun, await, fun(_, _, _) -> - Result - end). - --spec mock_gun_await_body(term()) -> ok. -mock_gun_await_body(Body) -> - meck:expect(gun, await_body, fun(_, _, _) -> - {ok, Body} +-spec mock_http2_post() -> ok. +mock_http2_post() -> + meck:expect(h2_client, send_request, fun(_, _, _) -> + self() ! {'END_STREAM', 1}, + {ok, 1} end). --spec mock_gun_await_up(term()) -> ok. -mock_gun_await_up(Result) -> - meck:expect(gun, await_up, fun(_, _) -> - Result +-spec mock_http2_get_response(integer(), list(), list()) -> ok. +mock_http2_get_response(ResponseCode, ResponseHeaders, Body) -> + meck:expect(h2_client, get_response, fun(_, _) -> + {ok, {[{<<":status">>, integer_to_binary(ResponseCode)} | ResponseHeaders], Body}} end). -spec maybe_mock_apns_os() -> ok.