diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index bed5f24..8e8c013 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -14,9 +14,43 @@ defmodule Chronoscope.NTS do end def list() do + call_all_clients(:list) + end + + def clear() do + :terminate + |> call_all_clients() + |> Enum.map(&wait_for_termination/1) + end + + defp wait_for_termination(pid) do + ref = Process.monitor(pid) + + receive do + {:DOWN, ^ref, _, _, _} -> {:ok, pid} + after + 1000 -> {:error, pid} + end + end + + defp call_all_clients(message) do NTS.DynamicSupervisor |> DynamicSupervisor.which_children() - |> Enum.map(fn {_, pid, _, _} -> GenServer.call(pid, :list) end) + |> Enum.map(fn {_, pid, _, _} -> GenServer.call(pid, message) end) + end + + def remove(host, port) do + name = "#{host}:#{port}" + + case Registry.lookup(NTS.Registry, name) do + [{pid, _}] -> + pid + |> GenServer.call(:terminate) + |> wait_for_termination() + + [] -> + {:error, :notfound} + end end @impl true diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index cf52078..71dc28b 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -1,5 +1,5 @@ defmodule Chronoscope.NTS.Client do - use GenServer + use GenServer, restart: :transient alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentClient @@ -23,6 +23,11 @@ defmodule Chronoscope.NTS.Client do }} end + @impl true + def handle_call(:terminate, _from, state) do + {:stop, :normal, self(), state} + end + @impl true def handle_call(:list, _from, state) do {:reply, state, state} diff --git a/test/chronoscope/nts/client_test.exs b/test/chronoscope/nts/client_test.exs index 2ab83f3..e05340e 100644 --- a/test/chronoscope/nts/client_test.exs +++ b/test/chronoscope/nts/client_test.exs @@ -10,18 +10,6 @@ defmodule Chronoscope.NTS.ClientTest do setup :verify_on_exit! - defp peercert() do - :secp256r1 - |> X509.PrivateKey.new_ec() - |> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test") - |> X509.Certificate.to_der() - end - - defp send_ssl_response(response) do - send(self(), {:ssl, nil, response}) - :ok - end - setup _tags do DateTimeMock |> stub(:utc_now, fn -> ~U[2024-03-31 01:23:45Z] end) diff --git a/test/chronoscope/nts/key_establishment_client_test.exs b/test/chronoscope/nts/key_establishment_client_test.exs index 180c9c9..2f58c72 100644 --- a/test/chronoscope/nts/key_establishment_client_test.exs +++ b/test/chronoscope/nts/key_establishment_client_test.exs @@ -12,18 +12,6 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do @timeout 3000 - defp peercert() do - :secp256r1 - |> X509.PrivateKey.new_ec() - |> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test") - |> X509.Certificate.to_der() - end - - defp send_ssl_response(response) do - send(self(), {:ssl, nil, response}) - :ok - end - describe "Chronoscope.NTS.KeyEstablishmentClient.key_establishment()" do test "sends the correct TLS options" do SSLMock diff --git a/test/chronoscope/nts_test.exs b/test/chronoscope/nts_test.exs new file mode 100644 index 0000000..9614077 --- /dev/null +++ b/test/chronoscope/nts_test.exs @@ -0,0 +1,72 @@ +defmodule Chronoscope.NTSTest do + use Chronoscope.Case + + alias Chronoscope.NTS.SSLMock + + import Chronoscope.NTS + import Mox + + setup :verify_on_exit! + setup :set_mox_global + + defp expect_key_establishment(host, port) do + host_charlist = to_charlist(host) + + SSLMock + |> expect(:connect, fn ^host_charlist, ^port, _, _ -> {:ok, :socket} end) + |> expect(:send, fn :socket, _ -> send_ssl_response([]) end) + |> expect(:peercert, fn :socket -> {:ok, peercert()} end) + |> expect(:close, fn :socket -> :ok end) + end + + setup do + clear() + on_exit(fn -> clear() end) + end + + describe "Chronoscope.NTS.healthy?()" do + test "is healthy" do + assert healthy?() == true + end + end + + describe "Chronoscope.NTS.list()" do + test "shows empty client list" do + assert list() == [] + end + + test "shows all clients" do + expect_key_establishment("localhost", 4444) + key_establishment("localhost", 4444) + + assert [%{host: "localhost", key_establishment_response: _, last_key_establishment: _, port: 4444}] = list() + end + end + + describe "Chronoscope.NTS.remove()" do + test "removes a client" do + expect_key_establishment("localhost", 4444) + key_establishment("localhost", 4444) + + assert {:ok, _} = remove("localhost", 4444) + assert list() == [] + end + + test "does nothing if the client doesn't exist" do + expect_key_establishment("localhost", 4444) + key_establishment("localhost", 4444) + + assert remove("localhost", 1111) == {:error, :notfound} + assert [%{host: "localhost", key_establishment_response: _, last_key_establishment: _, port: 4444}] = list() + end + end + + describe "Chronoscope.NTS.key_establishment()" do + test "creates and reuses a client" do + expect_key_establishment("localhost", 4444) + + assert {:ok, %{cert_expiration: _}} = key_establishment("localhost", 4444) + assert {:ok, %{cert_expiration: _}} = key_establishment("localhost", 4444) + end + end +end diff --git a/test/support/case.ex b/test/support/case.ex index 18a5228..ec1a8ba 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -1,8 +1,26 @@ defmodule Chronoscope.Case do use ExUnit.CaseTemplate + using do + quote do + import Chronoscope.Case + end + end + setup _tags do Mox.stub_with(Chronoscope.NTS.DateTimeMock, Chronoscope.DateTime.Stub) :ok end + + def peercert() do + :secp256r1 + |> X509.PrivateKey.new_ec() + |> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test") + |> X509.Certificate.to_der() + end + + def send_ssl_response(response) do + send(self(), {:ssl, nil, response}) + :ok + end end diff --git a/test/support/ssl.ex b/test/support/ssl.ex new file mode 100644 index 0000000..e69de29