defmodule OhMyExOhIrcd.IRCTestHelper do
@moduledoc """
Test helpers for spinning up isolated IRC listeners and driving
TCP clients against them.
Each test gets its own listener on an ephemeral port (`port: 0`) via
`ExUnit.Callbacks.start_supervised!/1`, so listener config (auth flag,
ping timers) can be customized per-test and tests don't share port
state.
"""
import ExUnit.Callbacks, only: [start_supervised!: 1]
@default_options %{
server_name: "test.irc",
network_name: "TestNet",
allow_anonymous: true,
motd: ["Test MOTD line one.", "Test MOTD line two."],
ping_interval_ms: 120_000,
ping_timeout_ms: 60_000
}
@doc """
Start an isolated IRC listener on an ephemeral port. Returns the bound
port. Listener is torn down at test exit via `start_supervised!`.
## Options
Anything in the handler options map (`:allow_anonymous`, `:motd`,
`:ping_interval_ms`, `:ping_timeout_ms`, `:server_name`,
`:network_name`). Unspecified keys fall back to test defaults.
"""
@spec start_listener(map()) :: pos_integer()
def start_listener(opts \\ %{}) do
handler_options = Map.merge(@default_options, opts)
spec =
ThousandIsland.child_spec(
port: 0,
handler_module: OhMyExOhIrcd.IRC.Connection,
read_timeout: 30_000,
handler_options: handler_options
)
pid = start_supervised!(spec)
{:ok, {_ip, port}} = ThousandIsland.listener_info(pid)
port
end
@doc "Open a line-mode TCP connection to the given port."
def connect(port) do
{:ok, sock} =
:gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false, packet: :line], 1_000)
sock
end
@doc "Send raw IRC lines (no need to include CRLF — we append it)."
def send_line(sock, line) when is_binary(line) do
:ok = :gen_tcp.send(sock, line <> "\r\n")
end
@doc """
Read up to `count` lines from the socket within `timeout_ms`. Each
returned element is the line *without* trailing CRLF.
"""
def recv_lines(sock, count, timeout_ms \\ 500) do
do_recv_lines(sock, count, timeout_ms, [])
end
defp do_recv_lines(_sock, 0, _t, acc), do: Enum.reverse(acc)
defp do_recv_lines(sock, n, t, acc) do
case :gen_tcp.recv(sock, 0, t) do
{:ok, line} -> do_recv_lines(sock, n - 1, t, [String.trim_trailing(line) | acc])
{:error, :timeout} -> Enum.reverse(acc)
{:error, :closed} -> Enum.reverse(acc)
end
end
@doc """
Drain everything readable until the socket goes quiet for `timeout_ms`.
"""
def drain(sock, timeout_ms \\ 200) do
Stream.repeatedly(fn -> :gen_tcp.recv(sock, 0, timeout_ms) end)
|> Enum.take_while(&match?({:ok, _}, &1))
|> Enum.map(fn {:ok, line} -> String.trim_trailing(line) end)
end
@doc """
Drive the registration handshake (NICK + USER), then drain the welcome
numerics. Returns the socket.
"""
def register(port, nick, opts \\ []) do
sock = connect(port)
realname = Keyword.get(opts, :realname, nick)
user = Keyword.get(opts, :user, nick)
send_line(sock, "NICK #{nick}")
send_line(sock, "USER #{user} 0 * :#{realname}")
# Drain welcome through end-of-MOTD (376 or 422).
drain(sock, 250)
sock
end
@doc "Close a connection cleanly."
def quit(sock, reason \\ "bye") do
send_line(sock, "QUIT :#{reason}")
:gen_tcp.close(sock)
end
@doc """
Generate a unique nick suitable for cross-test isolation (the Horde
registry is global).
"""
def unique_nick(prefix \\ "u") do
"#{prefix}#{System.unique_integer([:positive])}"
end
@doc """
Generate a unique channel name.
"""
def unique_channel(prefix \\ "test") do
"##{prefix}#{System.unique_integer([:positive])}"
end
@doc """
Drive the full SASL-PLAIN + NICK/USER handshake and return the socket
once registration completes. Drains all welcome / SASL numerics.
"""
def register_sasl(port, nick, email, password) do
sock = connect(port)
payload = Base.encode64(<<0>> <> email <> <<0>> <> password)
send_line(sock, "CAP LS")
send_line(sock, "CAP REQ :sasl")
send_line(sock, "AUTHENTICATE PLAIN")
send_line(sock, "AUTHENTICATE #{payload}")
send_line(sock, "CAP END")
send_line(sock, "NICK #{nick}")
send_line(sock, "USER #{nick} 0 * :#{nick}")
drain(sock, 400)
sock
end
end
neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.2 KiB · text
History
442a476