Show a list of NTS servers

This commit is contained in:
Mike Cifelli 2024-06-04 20:03:02 -04:00
parent 7226742a6b
commit 5109722a66
Signed by: mike
GPG Key ID: 6B08C6BE47D08E4C
20 changed files with 213 additions and 53 deletions

3
.gitignore vendored
View File

@ -37,3 +37,6 @@ npm-debug.log
# Ignore IDE files. # Ignore IDE files.
.elixir_ls/ .elixir_ls/
# Ignore server lists.
/priv/nts.txt

View File

@ -30,6 +30,9 @@ config :chronoscope, ChronoscopeWeb.Endpoint,
# at the `config/runtime.exs`. # at the `config/runtime.exs`.
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Local config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Local
config :chronoscope, :nts_topic, "nts-servers"
config :chronoscope, :nts_file, "priv/nts.txt"
# Configure esbuild (the version is required) # Configure esbuild (the version is required)
config :esbuild, config :esbuild,
version: "0.17.11", version: "0.17.11",

View File

@ -10,6 +10,8 @@ config :chronoscope, ChronoscopeWeb.Endpoint,
# In test we don't send emails. # In test we don't send emails.
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Test config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Test
config :chronoscope, :nts_file, "test/priv/nts.txt"
# Disable swoosh api client as it is only required for production adapters. # Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false config :swoosh, :api_client, false

View File

@ -22,7 +22,8 @@ defmodule Chronoscope.Application do
{Task.Supervisor, name: Chronoscope.Gemini.TaskSupervisor}, {Task.Supervisor, name: Chronoscope.Gemini.TaskSupervisor},
{Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]}, {Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]},
# Start to serve requests, typically the last entry # Start to serve requests, typically the last entry
ChronoscopeWeb.Endpoint ChronoscopeWeb.Endpoint,
Chronoscope.Monitor
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html

View File

@ -9,6 +9,8 @@ defmodule Chronoscope.Gemini do
alias Chronoscope.Gemini alias Chronoscope.Gemini
@timeout_in_milliseconds 10_000
@registry Application.compile_env(:chronoscope, :registry, Registry) @registry Application.compile_env(:chronoscope, :registry, Registry)
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer) @genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor) @dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
@ -20,14 +22,14 @@ defmodule Chronoscope.Gemini do
def list() do def list() do
Gemini.DynamicSupervisor Gemini.DynamicSupervisor
|> @dynamic_supervisor.which_children() |> @dynamic_supervisor.which_children()
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end) |> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list, @timeout_in_milliseconds) end)
end end
def remove(host, port, path) do def remove(host, port, path) do
name = client_name(%{host: host, port: port, path: path}) name = client_name(%{host: host, port: port, path: path})
case @registry.lookup(Gemini.Registry, name) do case @registry.lookup(Gemini.Registry, name) do
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)} [{pid, _}] -> {:ok, @genserver.call(pid, :terminate, @timeout_in_milliseconds)}
[] -> {:error, :not_found} [] -> {:error, :not_found}
end end
end end
@ -36,7 +38,7 @@ defmodule Chronoscope.Gemini do
def connect(host, port, path) do def connect(host, port, path) do
%{host: host, port: port, path: path} %{host: host, port: port, path: path}
|> client_pid() |> client_pid()
|> @genserver.call(:connect) |> @genserver.call(:connect, @timeout_in_milliseconds)
end end
defp client_pid(resource) do defp client_pid(resource) do

View File

@ -5,6 +5,7 @@ defmodule Chronoscope.Gemini.Client do
alias Chronoscope.Gemini.ConnectionClient alias Chronoscope.Gemini.ConnectionClient
@interval_in_seconds 30 @interval_in_seconds 30
@timeout_in_milliseconds 10_000
@date_time Application.compile_env(:chronoscope, :date_time, DateTime) @date_time Application.compile_env(:chronoscope, :date_time, DateTime)
@ -61,7 +62,7 @@ defmodule Chronoscope.Gemini.Client do
defp server_response(%{resource: resource}) do defp server_response(%{resource: resource}) do
Gemini.TaskSupervisor Gemini.TaskSupervisor
|> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end) |> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end)
|> Task.await() |> Task.await(@timeout_in_milliseconds)
end end
defp interval_surpassed?(now, last_request) do defp interval_surpassed?(now, last_request) do

View File

@ -0,0 +1,40 @@
defmodule Chronoscope.Monitor do
use GenServer
require Logger
alias Chronoscope.NTS
alias Chronoscope.NTS.Parse
@nts_file Application.compile_env(:chronoscope, :nts_file)
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(_) do
File.touch(@nts_file)
activate_nts_clients()
{:ok, %{nts_servers: activate_nts_clients()}}
end
@impl true
def handle_info(:activate_clients, _state) do
{:noreply, %{nts_servers: activate_nts_clients()}}
end
@impl true
def handle_call(:get_nts_servers, _from, state) do
{:reply, state.nts_servers, state}
end
defp activate_nts_clients() do
@nts_file
|> File.stream!()
|> Stream.map(&String.trim/1)
|> Stream.map(&Parse.parse_nts_server/1)
|> Enum.to_list()
|> tap(fn server -> Enum.each(server, &NTS.auto_refresh/1) end)
end
end

View File

@ -9,10 +9,11 @@ defmodule Chronoscope.NTS do
alias Chronoscope.NTS alias Chronoscope.NTS
@timeout_in_milliseconds 10_000
@registry Application.compile_env(:chronoscope, :registry, Registry) @registry Application.compile_env(:chronoscope, :registry, Registry)
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer) @genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor) @dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
@topic "nts-servers"
def healthy?() do def healthy?() do
true true
@ -24,11 +25,29 @@ defmodule Chronoscope.NTS do
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end) |> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
end end
def list_clients(clients) do
clients
|> Enum.map(&client_pid/1)
|> Enum.map(fn pid -> @genserver.call(pid, :list, @timeout_in_milliseconds) end)
end
def auto_refresh(server) do
server
|> client_pid()
|> @genserver.call(:auto_refresh, @timeout_in_milliseconds)
end
def cancel_auto_refresh(server) do
server
|> client_pid()
|> @genserver.call(:cancel_auto_refresh, @timeout_in_milliseconds)
end
def remove(host, port) do def remove(host, port) do
name = client_name(%{host: host, port: port}) name = client_name(%{host: host, port: port})
case @registry.lookup(NTS.Registry, name) do case @registry.lookup(NTS.Registry, name) do
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)} [{pid, _}] -> {:ok, @genserver.call(pid, :terminate, @timeout_in_milliseconds)}
[] -> {:error, :not_found} [] -> {:error, :not_found}
end end
end end
@ -37,8 +56,7 @@ defmodule Chronoscope.NTS do
def key_establishment(host, port) do def key_establishment(host, port) do
%{host: host, port: port} %{host: host, port: port}
|> client_pid() |> client_pid()
|> @genserver.call(:key_establishment) |> @genserver.call(:key_establishment, @timeout_in_milliseconds)
|> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end)
end end
defp client_pid(server) do defp client_pid(server) do

View File

@ -5,7 +5,10 @@ defmodule Chronoscope.NTS.Client do
alias Chronoscope.NTS.KeyEstablishmentClient alias Chronoscope.NTS.KeyEstablishmentClient
@interval_in_seconds 30 @interval_in_seconds 30
@timeout_in_milliseconds 10_000
@refresh_interval_in_milliseconds :timer.minutes(1)
@topic Application.compile_env(:chronoscope, :nts_topic)
@date_time Application.compile_env(:chronoscope, :date_time, DateTime) @date_time Application.compile_env(:chronoscope, :date_time, DateTime)
def start_link(server: server, name: name) do def start_link(server: server, name: name) do
@ -34,6 +37,28 @@ defmodule Chronoscope.NTS.Client do
{:reply, state, state} {:reply, state, state}
end end
@impl true
def handle_call(:auto_refresh, _from, %{timer: _timer} = state) do
{:reply, :already_started, state}
end
@impl true
def handle_call(:auto_refresh, _from, state) do
{:ok, timer} = :timer.send_interval(@refresh_interval_in_milliseconds, :key_establishment)
{:reply, :ok, Map.put(state, :timer, timer)}
end
@impl true
def handle_call(:cancel_auto_refresh, _from, %{timer: timer} = state) do
:timer.cancel(timer)
{:reply, :ok, Map.delete(state, :timer)}
end
@impl true
def handle_call(:cancel_auto_refresh, _from, state) do
{:reply, :already_cancelled, state}
end
@impl true @impl true
def handle_call(:key_establishment, _from, state) do def handle_call(:key_establishment, _from, state) do
new_state = update_state(state) new_state = update_state(state)
@ -41,11 +66,18 @@ defmodule Chronoscope.NTS.Client do
{:reply, new_state.key_establishment_response, new_state} {:reply, new_state.key_establishment_response, new_state}
end end
@impl true
def handle_info(:key_establishment, state) do
{:noreply, update_state(state)}
end
defp update_state(state) do defp update_state(state) do
now = utc_now() now = utc_now()
if interval_surpassed?(now, state.last_key_establishment) do if interval_surpassed?(now, state.last_key_establishment) do
Map.merge(state, current_data(state, now)) state
|> Map.merge(current_data(state, now))
|> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end)
else else
state state
end end
@ -61,7 +93,7 @@ defmodule Chronoscope.NTS.Client do
defp server_response(%{server: server}) do defp server_response(%{server: server}) do
NTS.TaskSupervisor NTS.TaskSupervisor
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end) |> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|> Task.await() |> Task.await(@timeout_in_milliseconds)
end end
defp interval_surpassed?(now, last_key_establishment) do defp interval_surpassed?(now, last_key_establishment) do

View File

@ -5,7 +5,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do
alias Chronoscope.NTS.KeyEstablishmentRequest alias Chronoscope.NTS.KeyEstablishmentRequest
alias Chronoscope.NTS.KeyEstablishmentResponse alias Chronoscope.NTS.KeyEstablishmentResponse
@timeout_in_milliseconds 3000 @timeout_in_milliseconds 3500
@ssl Application.compile_env(:chronoscope, :ssl, :ssl) @ssl Application.compile_env(:chronoscope, :ssl, :ssl)

View File

@ -0,0 +1,10 @@
defmodule Chronoscope.NTS.Parse do
@default_port 4460
def parse_nts_server(server) do
case String.split(server, ":") do
[host, port] -> %{host: host, port: String.to_integer(port)}
[host] -> %{host: host, port: @default_port}
end
end
end

View File

@ -1,4 +1,4 @@
<header class="px-4 sm:px-6 lg:px-8"> <header class="px-4 sm:px-6 lg:px-8 sticky top-0 z-10 bg-white dark:bg-zinc-800">
<div class="flex items-center justify-between border-b border-zinc-100 dark:border-zinc-700 py-3 text-sm"> <div class="flex items-center justify-between border-b border-zinc-100 dark:border-zinc-700 py-3 text-sm">
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300 pe-4"> <div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300 pe-4">
<a href="/" class="hover:text-zinc-700 dark:hover:text-zinc-400"> <a href="/" class="hover:text-zinc-700 dark:hover:text-zinc-400">

View File

@ -4,14 +4,20 @@ defmodule ChronoscopeWeb.IndexLive do
alias Chronoscope.NTS alias Chronoscope.NTS
alias Chronoscope.NTS.KeyEstablishmentResponse alias Chronoscope.NTS.KeyEstablishmentResponse
@topic "nts-servers" @topic Application.compile_env(:chronoscope, :nts_topic)
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
ChronoscopeWeb.Endpoint.subscribe(@topic) ChronoscopeWeb.Endpoint.subscribe(@topic)
{:ok, assign(socket, %{servers: NTS.list()})} {:ok, assign(socket, %{servers: server_list()})}
end end
def handle_info(%{topic: @topic}, socket) do def handle_info(%{topic: @topic}, socket) do
{:noreply, assign(socket, %{servers: NTS.list()})} {:noreply, assign(socket, %{servers: server_list()})}
end
defp server_list() do
Chronoscope.Monitor
|> GenServer.call(:get_nts_servers)
|> NTS.list_clients()
end end
end end

View File

@ -2,31 +2,31 @@
<table class="mx-auto border-collapse table-auto min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 text-left"> <table class="mx-auto border-collapse table-auto min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 text-left">
<thead> <thead>
<tr> <tr>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
NTS-KE Server NTS-KE Server
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Status Status
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Algorithm Algorithm
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Cookies Cookies
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Cookie Length Cookie Length
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
NTP Host NTP Host
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
NTP Port NTP Port
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Certificate Expiration Certificate Expiration
</th> </th>
<th scope="col" class="py-3 px-6 whitespace-nowrap"> <th scope="col" class="py-2 px-6 whitespace-nowrap">
Last Check Last Check
</th> </th>
</tr> </tr>
@ -36,13 +36,13 @@
<% {status, response} = server.key_establishment_response %> <% {status, response} = server.key_establishment_response %>
<%= if (status == :ok) do %> <%= if (status == :ok) do %>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= server.server.host %>:<%= server.server.port %> <%= server.server.host %><span :if={server.server.port != 4460}>:<%= server.server.port %></span>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= status %> <%= status %>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<% aead_algorithm = Enum.at(response.aead_algorithms, 0) %> <% aead_algorithm = Enum.at(response.aead_algorithms, 0) %>
<span class="group relative"> <span class="group relative">
@ -52,30 +52,30 @@
</span> </span>
</span> </span>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= length(response.cookies) %> <%= length(response.cookies) %>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= response.cookie_length %> <%= response.cookie_length %>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= Map.get(response, :server, "-") %> <%= Map.get(response, :server, "-") %>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= Map.get(response, :port, "-") %> <%= Map.get(response, :port, "-") %>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%> <%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %>
</td> </td>
<% else %> <% else %>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= server.server.host %>:<%= server.server.port %> <%= server.server.host %><span :if={server.server.port != 4460}>:<%= server.server.port %></span>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<span class="group relative"> <span class="group relative">
<%= status %> <%= status %>
<span class="pointer-events-none absolute -top-9 left-0 w-max p-1 rounded-lg bg-zinc-300 dark:bg-zinc-600 opacity-0 transition-opacity group-hover:opacity-100"> <span class="pointer-events-none absolute -top-9 left-0 w-max p-1 rounded-lg bg-zinc-300 dark:bg-zinc-600 opacity-0 transition-opacity group-hover:opacity-100">
@ -83,13 +83,13 @@
</span> </span>
</span> </span>
</td> </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> - </td> <td class="py-2 px-6 whitespace-nowrap"> - </td>
<td class="py-4 px-6 whitespace-nowrap"> <td class="py-2 px-6 whitespace-nowrap">
<%= <%=
server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ")
%> %>

View File

@ -4,7 +4,7 @@ defmodule Chronoscope.MixProject do
def project do def project do
[ [
app: :chronoscope, app: :chronoscope,
version: "0.1.0", version: "1.0.0",
elixir: "~> 1.16", elixir: "~> 1.16",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,

View File

@ -0,0 +1,15 @@
defmodule Chronoscope.NTS.ParseTest do
use Chronoscope.Case, async: true
import Chronoscope.NTS.Parse
describe "Chronoscope.NTS.Parse.parse_nts_server()" do
test "parses a server with a port" do
assert parse_nts_server("test.example.com:1234") == %{host: "test.example.com", port: 1234}
end
test "parses a server without a port" do
assert parse_nts_server("test.example.com") == %{host: "test.example.com", port: 4460}
end
end
end

View File

@ -36,6 +36,37 @@ defmodule Chronoscope.NTSTest do
end end
end end
describe "Chronoscope.NTS.list_clients()" do
test "shows empty client list" do
assert NTS.list_clients([]) == []
end
test "lists the given clients and starts them if they don't exist" do
RegistryMock
|> expect(:lookup, fn _, "test:1" -> [{1, nil}] end)
|> expect(:lookup, fn _, "test2:2" -> [] end)
DynamicSupervisorMock
|> expect(
:start_child,
fn Chronoscope.NTS.DynamicSupervisor,
{Chronoscope.NTS.Client,
[
server: %{host: "test2", port: 2},
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "test2:2"}}
]} ->
{:ok, 2}
end
)
GenServerMock
|> expect(:call, fn 1, :list -> :one end)
|> expect(:call, fn 2, :list -> :two end)
assert NTS.list_clients([%{host: "test", port: 1}, %{host: "test2", port: 2}]) == [:one, :two]
end
end
describe "Chronoscope.NTS.remove()" do describe "Chronoscope.NTS.remove()" do
test "does nothing if the client doesn't exist" do test "does nothing if the client doesn't exist" do
RegistryMock RegistryMock
@ -74,7 +105,7 @@ defmodule Chronoscope.NTSTest do
) )
GenServerMock GenServerMock
|> expect(:call, fn 1, :key_establishment -> :result end) |> expect(:call, fn 1, :key_establishment, 10_000 -> :result end)
assert NTS.key_establishment("localhost", 1111) == :result assert NTS.key_establishment("localhost", 1111) == :result
end end
@ -84,7 +115,7 @@ defmodule Chronoscope.NTSTest do
|> expect(:lookup, fn _, _ -> [{1, 2}] end) |> expect(:lookup, fn _, _ -> [{1, 2}] end)
GenServerMock GenServerMock
|> expect(:call, fn 1, :key_establishment -> :result end) |> expect(:call, fn 1, :key_establishment, 10_000 -> :result end)
assert NTS.key_establishment("localhost", 1111) == :result assert NTS.key_establishment("localhost", 1111) == :result
end end

View File

@ -1,16 +1,11 @@
defmodule ChronoscopeWeb.PageControllerTest do defmodule ChronoscopeWeb.PageControllerTest do
use ChronoscopeWeb.ConnCase, async: true use ChronoscopeWeb.ConnCase, async: true
alias Chronoscope.DynamicSupervisorMock
import Mox import Mox
setup :verify_on_exit! setup :verify_on_exit!
test "GET /", %{conn: conn} do test "GET /", %{conn: conn} do
DynamicSupervisorMock
|> expect(:which_children, fn _ -> [] end)
conn = get(conn, ~p"/") conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Chronoscope" assert html_response(conn, 200) =~ "Chronoscope"
end end

0
test/priv/nts.txt Normal file
View File

View File

@ -19,5 +19,6 @@ defmodule Chronoscope.DynamicSupervisor.Behaviour do
end end
defmodule Chronoscope.GenServer.Behaviour do defmodule Chronoscope.GenServer.Behaviour do
@callback call(pid(), any(), any()) :: any()
@callback call(pid(), any()) :: any() @callback call(pid(), any()) :: any()
end end