Skip to content

Commit

Permalink
feat: @callback handle_CAPABILITIES/1
Browse files Browse the repository at this point in the history
This callback needs to return a list of capabilities the server can do.
The list will be joined into a 101 response.
  • Loading branch information
sntran committed Mar 2, 2021
1 parent 94b0820 commit 4a12906
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 12 deletions.
2 changes: 2 additions & 0 deletions lib/gen_nntp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ defmodule GenNNTP do
{:ok, state} | {:ok, state, timeout | :hibernate} |
:ignore | {:stop, reason :: term}

@callback handle_CAPABILITIES(state) :: {:ok, capabilities :: [String.t()], state}

@callback handle_command(command :: String.t(), state) ::
{:reply, response :: any(), state} |
{:noreply, state} |
Expand Down
36 changes: 29 additions & 7 deletions src/gen_nntp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@
}).

-callback init(Args :: term()) ->
{ok, State :: state()}
{ok, state()}
| ignore
| {stop, Reason :: any()}.

-callback handle_command(Command :: binary(), State :: state()) ->
{reply, Response :: binary(), NewState}
| {noreply, NewState}
| {stop, Reason :: any(), NewState}
| {stop, Reason :: any, Response :: binary(), NewState}
when NewState :: state().
-callback handle_CAPABILITIES(state()) -> {ok, Capabilities :: [binary()], state()}.

-callback handle_command(Command :: binary(), state()) ->
{reply, Response :: binary(), state()}
| {noreply, state()}
| {stop, Reason :: any(), state()}
| {stop, Reason :: any, Response :: binary(), state()}.

%% ==================================================================
%% API
Expand Down Expand Up @@ -197,6 +198,21 @@ handle_info(timeout, #client{module =Module, transport = Transport} = Client) ->
Transport:send(Socket, "200 Service available, posting allowed\r\n"),
{noreply, Client};

% Client asks for server's capabilities. Responds with 101 code.
% Follows with the capabilities returned from `handle_CAPABILITIES/1` callback.
handle_info({tcp, Socket, <<"CAPABILITIES\r\n">>}, #client{transport = Transport} = Client) ->
#client{transport = Transport, module = Module, state = State} = Client,
% Asks the callback module to provide the capacitities at this moment.
{ok, Capabilities, State1} = Module:handle_CAPABILITIES(State),

% Build multi-line data block responsefollowing the 101 response code.
Response = join(<<"\r\n">>, [<<"101 Capability list:">> | Capabilities]),

% Ends the multi-line data block with a termination line.
Transport:send(Socket, <<Response/binary, "\r\n.\r\n">>),

{noreply, Client#client{state = State1}};

% The client uses the QUIT command to terminate the session. The server
% MUST acknowledge the QUIT command and then close the connection to
% the client.
Expand Down Expand Up @@ -257,3 +273,9 @@ terminate(_, _Client) -> ok.
code_change(_OldVsn, Client, _Extra) ->
% ..code to convert state (and more) during code change
{ok, Client}.

% Join binary
join(_Separator, []) ->
<<>>;
join(Separator, [H|T]) ->
lists:foldl(fun (Value, Acc) -> <<Acc/binary, Separator/binary, Value/binary>> end, H, T).
60 changes: 60 additions & 0 deletions test/gen_nntp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,66 @@ defmodule GenNNTPTest do

end

describe "@callback handle_CAPABILITIES/1" do

setup context do
capabilities = context[:capabilities] || []

TestNNTPServer.start(
handle_CAPABILITIES: fn(state) ->
Kernel.send(:tester, {:called_back, :handle_CAPABILITIES, 1})
{:ok, capabilities, state}
end
)

{:ok, socket, _greeting} = GenNNTP.connect()

%{socket: socket}
end

test "is called when the client asks for it", %{socket: socket} do

refute_receive(
{:called_back, :handle_CAPABILITIES, 1},
100,
"@callback handle_CAPABILITIES/1 should not be called when client has not asked for it"
)

:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")

assert_receive(
{:called_back, :handle_CAPABILITIES, 1},
100,
"@callback handle_CAPABILITIES/1 was not called"
)
end

test "is responded with 101 code", %{socket: socket} do
:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^101 /
end

@tag capabilities: ["VERSION 2", "HDR", "IHAVE", "LIST", "MODE-READER", "NEWNEWS", "OVER", "POST", "READER"]
test "is responded with capabilities returned from the callback", %{socket: socket, capabilities: capabilities} do
:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^101 /

for capability <- capabilities do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "#{capability}\r\n"
end
# Receives the termination line.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === ".\r\n"

# Should not receive any other message.
assert {:error, :timeout} = :gen_tcp.recv(socket, 0, 100)
end

end

describe "server interaction" do
setup do
TestNNTPServer.start()
Expand Down
18 changes: 13 additions & 5 deletions test/support/test_nntp_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ defmodule TestNNTPServer do
@behaviour GenNNTP

@type callback :: {atom(), fun}
@typep state :: any()

## API
@spec start([callback], [GenNNTP.option()]) :: :ignore | {:error, any()} | {:ok, pid()}
Expand All @@ -23,10 +22,6 @@ defmodule TestNNTPServer do
## GenNNTP callbacks

@impl GenNNTP
@spec init(keyword()) ::
{:ok, state}
| :ignore
| {:stop, reason :: term}
def init(options \\ []) do
# Init arguments if any.
args = Keyword.get(options, :args)
Expand All @@ -45,6 +40,19 @@ defmodule TestNNTPServer do
end
end

@impl GenNNTP
def handle_CAPABILITIES(client) do
state = client[:state]

case maybe_apply(client, :handle_CAPABILITIES, [state], {:ok, [], state}) do
{:ok, capabilities, state} ->
client = Keyword.put(client, :state, state)
{:ok, capabilities, client}
other ->
other
end
end

@impl GenNNTP
def handle_command(_command, state) do
{:noreply, state}
Expand Down

0 comments on commit 4a12906

Please sign in to comment.