diff --git a/Dockerfile.dev b/Dockerfile.dev index 043821d4f..b1af73609 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -47,6 +47,7 @@ COPY apps/fz_wall/mix.exs /var/app/apps/fz_wall/mix.exs COPY mix.exs /var/app/mix.exs COPY mix.lock /var/app/mix.lock RUN mix do deps.get, deps.compile, compile +RUN mix do deps.get --only test, deps.compile --only test, compile --only test COPY apps /var/app/apps COPY config /var/app/config diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex index 79f7cf867..9d4578bee 100644 --- a/apps/fz_http/lib/fz_http/devices.ex +++ b/apps/fz_http/lib/fz_http/devices.ex @@ -11,6 +11,16 @@ defmodule FzHttp.Devices do require Logger + def count_active_within(duration_in_secs) when is_integer(duration_in_secs) do + cutoff = DateTime.add(DateTime.utc_now(), -1 * duration_in_secs) + + Repo.one( + from d in Device, + select: count(d.id), + where: d.latest_handshake > ^cutoff + ) + end + def count do Repo.one(from d in Device, select: count(d.id)) end diff --git a/apps/fz_http/lib/fz_http/telemetry.ex b/apps/fz_http/lib/fz_http/telemetry.ex index e6c1c8156..473d5afcb 100644 --- a/apps/fz_http/lib/fz_http/telemetry.ex +++ b/apps/fz_http/lib/fz_http/telemetry.ex @@ -74,9 +74,13 @@ defmodule FzHttp.Telemetry do telemetry_module().capture("ping", ping_data()) end + # How far back to count handshakes as an active device + @active_device_window 86_400 def ping_data do common_fields() ++ [ + devices_active_within_24h: Devices.count_active_within(@active_device_window), + admin_count: Users.count(role: :admin), user_count: Users.count(), device_count: Devices.count(), max_devices_for_users: Devices.max_count_by_user_id(), diff --git a/apps/fz_http/lib/fz_http/users.ex b/apps/fz_http/lib/fz_http/users.ex index 853d13b94..3d76af65d 100644 --- a/apps/fz_http/lib/fz_http/users.ex +++ b/apps/fz_http/lib/fz_http/users.ex @@ -17,6 +17,10 @@ defmodule FzHttp.Users do Repo.one(from u in User, select: count(u.id)) end + def count(role: role) do + Repo.one(from u in User, select: count(u.id), where: u.role == ^role) + end + def consume_sign_in_token(token) when is_binary(token) do case find_and_clear_token(token) do {:ok, {:ok, user}} -> {:ok, user} diff --git a/apps/fz_http/test/fz_http/devices_test.exs b/apps/fz_http/test/fz_http/devices_test.exs index c40c40886..c1535619f 100644 --- a/apps/fz_http/test/fz_http/devices_test.exs +++ b/apps/fz_http/test/fz_http/devices_test.exs @@ -1,7 +1,9 @@ defmodule FzHttp.DevicesTest do # XXX: Update the device IP query to be an insert use FzHttp.DataCase, async: false - alias FzHttp.{Devices, Users} + alias FzHttp.Devices + alias FzHttp.DevicesFixtures + alias FzHttp.Users describe "trimmed fields" do test "trims expected fields" do @@ -34,6 +36,23 @@ defmodule FzHttp.DevicesTest do end end + describe "count_active_within/1" do + @active_within 30 + + test "returns device count active within the last 30 seconds" do + DevicesFixtures.device(%{latest_handshake: DateTime.utc_now()}) + + assert Devices.count_active_within(@active_within) == 1 + end + + test "omits device active exceeding 30 seconds" do + latest_handshake = DateTime.add(DateTime.utc_now(), -31) + DevicesFixtures.device(%{latest_handshake: latest_handshake}) + + assert Devices.count_active_within(@active_within) == 0 + end + end + describe "list_devices/0" do setup [:create_device] diff --git a/apps/fz_http/test/fz_http/users_test.exs b/apps/fz_http/test/fz_http/users_test.exs index 2e8f2447c..5ddf46e89 100644 --- a/apps/fz_http/test/fz_http/users_test.exs +++ b/apps/fz_http/test/fz_http/users_test.exs @@ -3,6 +3,28 @@ defmodule FzHttp.UsersTest do alias FzHttp.{Repo, Users} + describe "count/0" do + setup :create_user + + test "returns correct count of all users" do + assert Users.count() == 1 + end + end + + describe "count/1" do + setup :create_users + + @tag count: 3 + test "returns the correct count of admin users" do + assert Users.count(role: :admin) == 3 + end + + @tag count: 7, role: :unprivileged + test "returns the correct count of unprivileged users" do + assert Users.count(role: :unprivileged) == 7 + end + end + describe "trimmed fields" do test "trims expected fields" do changeset = @@ -325,7 +347,7 @@ defmodule FzHttp.UsersTest do {:ok, user: user} end - @tag :unprivileged + @tag role: :unprivileged test "enable via OIDC", %{user: user} do Users.enable_vpn_connection(user, %{provider: :oidc}) @@ -334,7 +356,7 @@ defmodule FzHttp.UsersTest do assert %{disabled_at: nil} = user end - @tag :unprivileged + @tag role: :unprivileged test "no change via password", %{user: user} do Users.enable_vpn_connection(user, %{provider: :identity}) diff --git a/apps/fz_http/test/fz_http_web/live/user_live/vpn_connection_component_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/vpn_connection_component_test.exs index 8dbf60c3c..aba051112 100644 --- a/apps/fz_http/test/fz_http_web/live/user_live/vpn_connection_component_test.exs +++ b/apps/fz_http/test/fz_http_web/live/user_live/vpn_connection_component_test.exs @@ -14,7 +14,7 @@ defmodule FzHttpWeb.UserLive.VPNConnectionComponentTest do describe "unprivileged" do setup :create_user - @tag :unprivileged + @tag role: :unprivileged test "checkbox is not disabled", %{user: user} do refute render_component(VPNConnectionComponent, id: "1", user: user) =~ ~r"\bdisabled\b" end diff --git a/apps/fz_http/test/support/test_helpers.ex b/apps/fz_http/test/support/test_helpers.ex index d23726ae5..e30b311ee 100644 --- a/apps/fz_http/test/support/test_helpers.ex +++ b/apps/fz_http/test/support/test_helpers.ex @@ -91,12 +91,8 @@ defmodule FzHttp.TestHelpers do end def create_user(tags) do - user = - if tags[:unprivileged] do - UsersFixtures.user(%{role: :unprivileged}) - else - UsersFixtures.user() - end + role = tags[:role] || :admin + user = UsersFixtures.user(%{role: role}) {:ok, user: user} end @@ -208,12 +204,13 @@ defmodule FzHttp.TestHelpers do })} end - def create_users(opts) do - count = opts[:count] || 5 + def create_users(tags) do + count = tags[:count] || 5 + role = tags[:role] || :admin users = Enum.map(1..count, fn i -> - UsersFixtures.user(%{email: "userlist#{i}@test"}) + UsersFixtures.user(%{role: role, email: "userlist#{i}@test"}) end) {:ok, users: users}