Add GeoIP import HTTP tests
This commit is contained in:
parent
9d513b77ee
commit
495b1a75b2
20 changed files with 424 additions and 64 deletions
|
|
@ -42,6 +42,7 @@ defmodule Mix.Tasks.Geoip.Import do
|
|||
|
||||
alias Towerops.GeoIP.Block
|
||||
alias Towerops.GeoIP.Location
|
||||
alias Towerops.HTTP
|
||||
alias Towerops.Repo
|
||||
|
||||
# PostgreSQL has a 65,535 parameter limit
|
||||
|
|
@ -202,19 +203,22 @@ defmodule Mix.Tasks.Geoip.Import do
|
|||
"truncate" => truncate
|
||||
}
|
||||
|
||||
response =
|
||||
Req.post!(url,
|
||||
json: body,
|
||||
headers: [{"authorization", "Bearer #{api_key}"}],
|
||||
retry: :transient
|
||||
)
|
||||
case HTTP.post(__MODULE__, url,
|
||||
json: body,
|
||||
headers: [{"authorization", "Bearer #{api_key}"}],
|
||||
retry: :transient
|
||||
) do
|
||||
{:ok, %{status: 200, body: response_body}} ->
|
||||
response_body
|
||||
|
||||
if response.status != 200 do
|
||||
Mix.shell().error("API request failed: #{inspect(response.body)}")
|
||||
raise "Failed to send batch to API"
|
||||
{:ok, %{body: response_body}} ->
|
||||
Mix.shell().error("API request failed: #{inspect(response_body)}")
|
||||
raise "Failed to send batch to API"
|
||||
|
||||
{:error, reason} ->
|
||||
Mix.shell().error("API request failed: #{inspect(reason)}")
|
||||
raise "Failed to send batch to API"
|
||||
end
|
||||
|
||||
response.body
|
||||
end
|
||||
|
||||
defp import_from_directory(directory) do
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ defmodule Towerops.Accounts.HIBP do
|
|||
See: https://haveibeenpwned.com/API/v3#PwnedPasswords
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@api_url "https://api.pwnedpasswords.com/range/"
|
||||
|
|
@ -59,7 +61,7 @@ defmodule Towerops.Accounts.HIBP do
|
|||
defp fetch_breaches(prefix) do
|
||||
url = @api_url <> prefix
|
||||
|
||||
case Req.get(url, receive_timeout: @timeout) do
|
||||
case HTTP.get(__MODULE__, url, receive_timeout: @timeout) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
{:ok, body}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ defmodule Towerops.Agents.ReleaseChecker do
|
|||
defp fetch_and_cache do
|
||||
url = "https://api.github.com/repos/#{@github_repo}/releases/latest"
|
||||
|
||||
case Req.get(url, headers: [{"accept", "application/vnd.github+json"}]) do
|
||||
case req_get(url, headers: [{"accept", "application/vnd.github+json"}]) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
release = parse_release(body)
|
||||
:persistent_term.put({__MODULE__, :release}, {release, System.monotonic_time(:millisecond)})
|
||||
|
|
@ -120,7 +120,7 @@ defmodule Towerops.Agents.ReleaseChecker do
|
|||
sha_asset = Enum.find(all_assets, fn a -> a["name"] == "#{binary_name}.sha256" end)
|
||||
|
||||
if sha_asset do
|
||||
case Req.get(sha_asset["browser_download_url"]) do
|
||||
case req_get(sha_asset["browser_download_url"]) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
body |> String.trim() |> String.split(" ") |> List.first()
|
||||
|
||||
|
|
@ -129,4 +129,22 @@ defmodule Towerops.Agents.ReleaseChecker do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp req_get(url, opts \\ []) do
|
||||
opts =
|
||||
if Application.get_env(:towerops, :env) == :test do
|
||||
Keyword.put(opts, :plug, {Req.Test, __MODULE__})
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
Req.get(url, opts)
|
||||
rescue
|
||||
error in RuntimeError ->
|
||||
if String.contains?(Exception.message(error), "cannot find mock/stub") do
|
||||
{:error, :no_test_stub}
|
||||
else
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -136,31 +136,25 @@ defmodule Towerops.Billing.StripeClient do
|
|||
Returns {:ok, price_object} or {:error, reason}.
|
||||
"""
|
||||
def create_price(unit_amount_dollars) when is_binary(unit_amount_dollars) do
|
||||
# Convert dollars to cents (Stripe expects cents)
|
||||
# "2.00" -> Decimal.new("2.00") -> Decimal.mult(100) -> "200.00"
|
||||
unit_amount_cents =
|
||||
unit_amount_dollars
|
||||
|> Decimal.new()
|
||||
|> Decimal.mult(Decimal.new("100"))
|
||||
|> Decimal.to_string()
|
||||
|
||||
params = %{
|
||||
product: stripe_product_id(),
|
||||
currency: "usd",
|
||||
unit_amount_decimal: unit_amount_cents,
|
||||
recurring: %{
|
||||
interval: "month",
|
||||
usage_type: "metered",
|
||||
aggregate_usage: "max"
|
||||
},
|
||||
billing_scheme: "per_unit",
|
||||
metadata: %{
|
||||
created_by: "towerops_admin_ui",
|
||||
created_at: DateTime.to_iso8601(DateTime.utc_now())
|
||||
with {:ok, unit_amount_cents} <- dollars_to_cents(unit_amount_dollars) do
|
||||
params = %{
|
||||
product: stripe_product_id(),
|
||||
currency: "usd",
|
||||
unit_amount_decimal: unit_amount_cents,
|
||||
recurring: %{
|
||||
interval: "month",
|
||||
usage_type: "metered",
|
||||
aggregate_usage: "max"
|
||||
},
|
||||
billing_scheme: "per_unit",
|
||||
metadata: %{
|
||||
created_by: "towerops_admin_ui",
|
||||
created_at: DateTime.to_iso8601(DateTime.utc_now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post("/v1/prices", params)
|
||||
post("/v1/prices", params)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -193,6 +187,18 @@ defmodule Towerops.Billing.StripeClient do
|
|||
request(:post, path, body: encoded)
|
||||
end
|
||||
|
||||
defp dollars_to_cents(unit_amount_dollars) do
|
||||
unit_amount_cents =
|
||||
unit_amount_dollars
|
||||
|> Decimal.new()
|
||||
|> Decimal.mult(Decimal.new("100"))
|
||||
|> Decimal.to_string()
|
||||
|
||||
{:ok, unit_amount_cents}
|
||||
rescue
|
||||
Decimal.Error -> {:error, {:bad_request, "Invalid amount"}}
|
||||
end
|
||||
|
||||
defp request(method, path, opts) do
|
||||
url = @base_url <> path
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ defmodule Towerops.Geocoding do
|
|||
Supports both system-wide API keys and organization-specific encrypted API keys.
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@google_geocoding_url "https://maps.googleapis.com/maps/api/geocode/json"
|
||||
|
|
@ -49,7 +51,7 @@ defmodule Towerops.Geocoding do
|
|||
"key" => api_key
|
||||
}
|
||||
|
||||
case Req.get(@google_geocoding_url, params: params, timeout: 10_000) do
|
||||
case HTTP.get(__MODULE__, @google_geocoding_url, params: params, timeout: 10_000) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
{:ok, body}
|
||||
|
||||
|
|
|
|||
44
lib/towerops/http.ex
Normal file
44
lib/towerops/http.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule Towerops.HTTP do
|
||||
@moduledoc false
|
||||
|
||||
def request(owner, req_opts) when is_list(req_opts) do
|
||||
req_opts
|
||||
|> maybe_attach_test_plug(owner)
|
||||
|> Req.request()
|
||||
rescue
|
||||
error in RuntimeError ->
|
||||
if String.contains?(Exception.message(error), "cannot find mock/stub") do
|
||||
{:error, :no_test_stub}
|
||||
else
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
def get(owner, url_or_opts, opts \\ []) do
|
||||
simple_request(:get, owner, url_or_opts, opts)
|
||||
end
|
||||
|
||||
def post(owner, url_or_opts, opts \\ []) do
|
||||
simple_request(:post, owner, url_or_opts, opts)
|
||||
end
|
||||
|
||||
def delete(owner, url_or_opts, opts \\ []) do
|
||||
simple_request(:delete, owner, url_or_opts, opts)
|
||||
end
|
||||
|
||||
defp simple_request(method, owner, url, opts) when is_binary(url) do
|
||||
request(owner, [method: method, url: url] ++ opts)
|
||||
end
|
||||
|
||||
defp simple_request(method, owner, req_opts, []) when is_list(req_opts) do
|
||||
request(owner, Keyword.put_new(req_opts, :method, method))
|
||||
end
|
||||
|
||||
defp maybe_attach_test_plug(req_opts, owner) do
|
||||
if Application.get_env(:towerops, :env) == :test and !Keyword.has_key?(req_opts, :plug) do
|
||||
Keyword.put(req_opts, :plug, {Req.Test, owner})
|
||||
else
|
||||
req_opts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -15,6 +15,8 @@ defmodule Towerops.Monitoring.Executors.HttpExecutor do
|
|||
}
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@default_timeout 5000
|
||||
|
|
@ -30,7 +32,7 @@ defmodule Towerops.Monitoring.Executors.HttpExecutor do
|
|||
start_time = System.monotonic_time(:millisecond)
|
||||
req_opts = build_req_opts(config, timeout_ms)
|
||||
|
||||
case Req.request(req_opts) do
|
||||
case HTTP.request(__MODULE__, req_opts) do
|
||||
{:ok, response} ->
|
||||
response_time = System.monotonic_time(:millisecond) - start_time
|
||||
validate_response(response, config, response_time)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Towerops.NetBox.Client do
|
|||
against a user-provided NetBox instance.
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
|
|
@ -82,7 +84,7 @@ defmodule Towerops.NetBox.Client do
|
|||
with {:ok, url} <- normalize_url(base_url) do
|
||||
full_url = url <> path
|
||||
|
||||
case Req.get(full_url,
|
||||
case HTTP.get(__MODULE__, full_url,
|
||||
headers: auth_headers(token),
|
||||
params: params,
|
||||
connect_options: [timeout: 10_000],
|
||||
|
|
@ -116,7 +118,7 @@ defmodule Towerops.NetBox.Client do
|
|||
with {:ok, url} <- normalize_url(base_url) do
|
||||
full_url = url <> path
|
||||
|
||||
case Req.post(full_url,
|
||||
case HTTP.post(__MODULE__, full_url,
|
||||
headers: auth_headers(token),
|
||||
json: body,
|
||||
connect_options: [timeout: 10_000],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Towerops.PagerDuty.Client do
|
||||
@moduledoc "PagerDuty Events API v2 client."
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@events_url "https://events.pagerduty.com/v2/enqueue"
|
||||
|
|
@ -75,7 +77,7 @@ defmodule Towerops.PagerDuty.Client do
|
|||
end
|
||||
|
||||
defp send_event(body) do
|
||||
case Req.post(@events_url, json: body) do
|
||||
case HTTP.post(__MODULE__, @events_url, json: body) do
|
||||
{:ok, %{status: status, body: resp_body}} when status in [200, 202] ->
|
||||
{:ok, resp_body}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Towerops.Security.CloudflareClient do
|
|||
pre-application blocking of brute force scanners.
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@base_url "https://api.cloudflare.com/client/v4"
|
||||
|
|
@ -30,7 +32,7 @@ defmodule Towerops.Security.CloudflareClient do
|
|||
|
||||
url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules"
|
||||
|
||||
case Req.post(url,
|
||||
case HTTP.post(__MODULE__, url,
|
||||
json: body,
|
||||
headers: [
|
||||
{"authorization", "Bearer #{api_token}"},
|
||||
|
|
@ -67,7 +69,7 @@ defmodule Towerops.Security.CloudflareClient do
|
|||
{:ok, rule_id} <- find_rule_by_ip(zone_id, api_token, ip_address) do
|
||||
url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules/#{rule_id}"
|
||||
|
||||
case Req.delete(url,
|
||||
case HTTP.delete(__MODULE__, url,
|
||||
headers: [
|
||||
{"authorization", "Bearer #{api_token}"}
|
||||
]
|
||||
|
|
@ -100,7 +102,7 @@ defmodule Towerops.Security.CloudflareClient do
|
|||
defp find_rule_by_ip(zone_id, api_token, ip_address) do
|
||||
url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules"
|
||||
|
||||
case Req.get(url,
|
||||
case HTTP.get(__MODULE__, url,
|
||||
headers: [
|
||||
{"authorization", "Bearer #{api_token}"}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Towerops.Weather.Client do
|
|||
Fetches weather by latitude/longitude — perfect for tower site locations.
|
||||
"""
|
||||
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@base_url "https://api.openweathermap.org/data/2.5"
|
||||
|
|
@ -25,7 +27,7 @@ defmodule Towerops.Weather.Client do
|
|||
|
||||
req_opts = maybe_add_test_plug([url: url, params: [lat: lat, lon: lon, appid: api_key]], opts)
|
||||
|
||||
case Req.get(req_opts) do
|
||||
case HTTP.get(__MODULE__, req_opts) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
{:ok, body}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
|
|||
import SweetXml
|
||||
|
||||
alias Towerops.Devices.Firmware
|
||||
alias Towerops.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
|
|||
Returns `{:ok, body}` on success or `{:error, reason}` on failure.
|
||||
"""
|
||||
def fetch_rss_feed do
|
||||
case Req.get(@rss_url, receive_timeout: 10_000) do
|
||||
case HTTP.get(__MODULE__, @rss_url, receive_timeout: 10_000) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
{:ok, body}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ defmodule ToweropsWeb.AgentLive.Show do
|
|||
put_flash(socket, :error, t("No binary available for architecture: %{arch}", arch: arch))
|
||||
|
||||
{:error, _reason} ->
|
||||
put_flash(socket, :error, t("Failed to fetch latest release from GitHub"))
|
||||
put_flash(socket, :error, t("Failed to fetch latest release"))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
|
||||
alias Towerops.Accounts
|
||||
alias Towerops.Accounts.Scope
|
||||
alias Towerops.HTTP
|
||||
alias Towerops.Organizations
|
||||
|
||||
@impl true
|
||||
|
|
@ -90,7 +91,7 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
url =
|
||||
"https://www.random.org/strings/?num=1&len=24&digits=on&upperalpha=on&loweralpha=on&unique=on&format=plain&rnd=new"
|
||||
|
||||
case Req.get(url) do
|
||||
case HTTP.get(__MODULE__, url) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
password = String.trim(body)
|
||||
{:ok, password}
|
||||
|
|
@ -99,10 +100,13 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
{:error, "random.org returned status #{status}"}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
{:error, format_request_error(error)}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_request_error(%_{} = error), do: Exception.message(error)
|
||||
defp format_request_error(error), do: inspect(error)
|
||||
|
||||
defp get_current_user(socket, session) do
|
||||
# Try to get current_user from socket assigns (if already set by on_mount)
|
||||
with nil <- Map.get(socket.assigns, :current_user),
|
||||
|
|
|
|||
|
|
@ -1,23 +1,87 @@
|
|||
defmodule Towerops.Agents.ReleaseCheckerTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Towerops.Agents.ReleaseChecker
|
||||
|
||||
setup do
|
||||
ReleaseChecker.invalidate_cache()
|
||||
|
||||
on_exit(fn ->
|
||||
ReleaseChecker.invalidate_cache()
|
||||
end)
|
||||
end
|
||||
|
||||
describe "get_latest_release/0" do
|
||||
@tag :integration
|
||||
test "returns release info with version and assets" do
|
||||
test "returns release info from the configured test stub" do
|
||||
Req.Test.stub(ReleaseChecker, fn conn ->
|
||||
if conn.request_path == "/repos/towerops-app/towerops-agent/releases/latest" do
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(
|
||||
200,
|
||||
Jason.encode!(%{
|
||||
"tag_name" => "v1.2.3",
|
||||
"assets" => [
|
||||
%{
|
||||
"name" => "towerops-agent-linux-amd64",
|
||||
"browser_download_url" => "https://example.com/amd64"
|
||||
},
|
||||
%{
|
||||
"name" => "towerops-agent-linux-amd64.sha256",
|
||||
"browser_download_url" => "https://example.com/amd64.sha256"
|
||||
},
|
||||
%{
|
||||
"name" => "towerops-agent-linux-arm64",
|
||||
"browser_download_url" => "https://example.com/arm64"
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
else
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("text/plain")
|
||||
|> Plug.Conn.send_resp(200, "abc123 towerops-agent-linux-amd64")
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, release} = ReleaseChecker.get_latest_release()
|
||||
assert release.version == "1.2.3"
|
||||
assert Enum.any?(release.assets, &(&1.name == "towerops-agent-linux-amd64"))
|
||||
assert Enum.any?(release.assets, &(&1.checksum == "abc123"))
|
||||
end
|
||||
|
||||
test "returns error when the API is unavailable" do
|
||||
Req.Test.stub(ReleaseChecker, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 503, "")
|
||||
end)
|
||||
|
||||
assert {:error, {:github_api, 503}} = ReleaseChecker.get_latest_release()
|
||||
end
|
||||
|
||||
test "uses cached release info until invalidated" do
|
||||
parent = self()
|
||||
|
||||
Req.Test.stub(ReleaseChecker, fn conn ->
|
||||
send(parent, {:release_request, conn.request_path})
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(
|
||||
200,
|
||||
Jason.encode!(%{
|
||||
"tag_name" => "v9.9.9",
|
||||
"assets" => []
|
||||
})
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, release} = ReleaseChecker.get_latest_release()
|
||||
assert {:ok, cached_release} = ReleaseChecker.get_latest_release()
|
||||
|
||||
assert is_binary(release.version)
|
||||
assert is_list(release.assets)
|
||||
|
||||
assert Enum.any?(release.assets, fn asset ->
|
||||
String.contains?(asset.name, "amd64")
|
||||
end)
|
||||
|
||||
assert Enum.any?(release.assets, fn asset ->
|
||||
String.contains?(asset.name, "arm64")
|
||||
end)
|
||||
assert release.version == "9.9.9"
|
||||
assert cached_release == release
|
||||
assert_receive {:release_request, "/repos/towerops-app/towerops-agent/releases/latest"}
|
||||
refute_receive {:release_request, "/repos/towerops-app/towerops-agent/releases/latest"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
30
test/towerops/http_test.exs
Normal file
30
test/towerops/http_test.exs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Towerops.HTTPTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.HTTP
|
||||
alias Towerops.HTTPTest.ExplicitStub
|
||||
|
||||
test "returns an error when no test stub is configured" do
|
||||
assert {:error, :no_test_stub} = HTTP.get(__MODULE__, "https://example.com")
|
||||
end
|
||||
|
||||
test "uses Req.Test stubs in test mode" do
|
||||
Req.Test.stub(__MODULE__, fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(200, Jason.encode!(%{"ok" => true}))
|
||||
end)
|
||||
|
||||
assert {:ok, %Req.Response{status: 200, body: %{"ok" => true}}} =
|
||||
HTTP.get(__MODULE__, "https://example.com")
|
||||
end
|
||||
|
||||
test "respects an explicit plug override" do
|
||||
Req.Test.stub(ExplicitStub, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 204, "")
|
||||
end)
|
||||
|
||||
assert {:ok, %Req.Response{status: 204}} =
|
||||
HTTP.get(__MODULE__, "https://example.com", plug: {Req.Test, ExplicitStub})
|
||||
end
|
||||
end
|
||||
80
test/towerops/maintenance/maintenance_window_test.exs
Normal file
80
test/towerops/maintenance/maintenance_window_test.exs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
defmodule Towerops.Maintenance.MaintenanceWindowTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
|
||||
describe "changeset/2" do
|
||||
test "is valid with required attributes" do
|
||||
changeset = MaintenanceWindow.changeset(%MaintenanceWindow{}, valid_attrs())
|
||||
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "requires ends_at to be after starts_at" do
|
||||
attrs =
|
||||
valid_attrs(%{
|
||||
ends_at: ~U[2026-03-10 10:00:00Z]
|
||||
})
|
||||
|
||||
changeset = MaintenanceWindow.changeset(%MaintenanceWindow{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert "must be after starts_at" in errors_on(changeset).ends_at
|
||||
end
|
||||
|
||||
test "validates name length" do
|
||||
changeset =
|
||||
MaintenanceWindow.changeset(
|
||||
%MaintenanceWindow{},
|
||||
valid_attrs(%{name: String.duplicate("a", 256)})
|
||||
)
|
||||
|
||||
refute changeset.valid?
|
||||
assert "should be at most 255 character(s)" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "validates reason length" do
|
||||
changeset =
|
||||
MaintenanceWindow.changeset(
|
||||
%MaintenanceWindow{},
|
||||
valid_attrs(%{reason: String.duplicate("a", 1001)})
|
||||
)
|
||||
|
||||
refute changeset.valid?
|
||||
assert "should be at most 1000 character(s)" in errors_on(changeset).reason
|
||||
end
|
||||
|
||||
test "allows ends_at to be absent while other required fields are present" do
|
||||
attrs = Map.delete(valid_attrs(), :ends_at)
|
||||
|
||||
changeset = MaintenanceWindow.changeset(%MaintenanceWindow{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert "can't be blank" in errors_on(changeset).ends_at
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_attrs(overrides \\ %{}) do
|
||||
Map.merge(
|
||||
%{
|
||||
name: "Fiber maintenance",
|
||||
reason: "Planned upgrade",
|
||||
starts_at: ~U[2026-03-10 10:00:00Z],
|
||||
ends_at: ~U[2026-03-10 11:00:00Z],
|
||||
organization_id: Ecto.UUID.generate(),
|
||||
created_by_id: Ecto.UUID.generate(),
|
||||
suppress_alerts: true,
|
||||
recurring: false
|
||||
},
|
||||
overrides
|
||||
)
|
||||
end
|
||||
|
||||
defp errors_on(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
||||
opts |> Keyword.fetch!(String.to_existing_atom(key)) |> to_string()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Towerops.Snmp.BatchInsertTest do
|
||||
use Towerops.DataCase, async: true
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
|
|
|
|||
53
test/towerops_web/components/consent_prompt_test.exs
Normal file
53
test/towerops_web/components/consent_prompt_test.exs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
defmodule ToweropsWeb.Components.ConsentPromptTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Phoenix.Component
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias ToweropsWeb.Components.ConsentPrompt
|
||||
|
||||
describe "consent_modal/1" do
|
||||
test "renders nothing when there are no policies" do
|
||||
assigns = %{user_id: "user-1", policies: []}
|
||||
|
||||
html =
|
||||
rendered_to_string(~H"""
|
||||
<ConsentPrompt.consent_modal user_id={@user_id} policies={@policies} />
|
||||
""")
|
||||
|
||||
assert html == ""
|
||||
end
|
||||
|
||||
test "renders translated consent checkboxes and links for known policies" do
|
||||
assigns = %{user_id: "user-1", policies: ["privacy_policy", "terms_of_service"]}
|
||||
|
||||
html =
|
||||
rendered_to_string(~H"""
|
||||
<ConsentPrompt.consent_modal user_id={@user_id} policies={@policies} />
|
||||
""")
|
||||
|
||||
assert html =~ ~s(id="consent-modal")
|
||||
assert html =~ ~s(phx-value-user-id="user-1")
|
||||
assert html =~ ~s(id="consent-privacy_policy")
|
||||
assert html =~ ~s(href="/privacy")
|
||||
assert html =~ "Privacy Policy"
|
||||
assert html =~ ~s(id="consent-terms_of_service")
|
||||
assert html =~ ~s(href="/terms")
|
||||
assert html =~ "Terms of Service"
|
||||
assert html =~ "Accept and Continue"
|
||||
end
|
||||
|
||||
test "falls back to the generic policy label and root link for unknown policies" do
|
||||
assigns = %{user_id: "user-1", policies: ["custom_policy"]}
|
||||
|
||||
html =
|
||||
rendered_to_string(~H"""
|
||||
<ConsentPrompt.consent_modal user_id={@user_id} policies={@policies} />
|
||||
""")
|
||||
|
||||
assert html =~ ~s(id="consent-custom_policy")
|
||||
assert html =~ ~s(href="/")
|
||||
assert html =~ "Policy"
|
||||
end
|
||||
end
|
||||
end
|
||||
42
test/towerops_web/telemetry_filter_test.exs
Normal file
42
test/towerops_web/telemetry_filter_test.exs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
defmodule ToweropsWeb.TelemetryFilterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias ToweropsWeb.TelemetryFilter
|
||||
|
||||
require Logger
|
||||
|
||||
describe "filter_health_checks/2" do
|
||||
test "stops logs for health check request paths" do
|
||||
log_event = {:info, self(), {Logger, "request completed", {{2026, 3, 10}, {12, 0, 0, 0}}, request_path: "/health"}}
|
||||
|
||||
assert :stop = TelemetryFilter.filter_health_checks(log_event, [])
|
||||
end
|
||||
|
||||
test "stops logs when phoenix logging is disabled" do
|
||||
log_event = {:info, self(), {Logger, "request completed", {{2026, 3, 10}, {12, 0, 0, 0}}, phoenix_log: false}}
|
||||
|
||||
assert :stop = TelemetryFilter.filter_health_checks(log_event, [])
|
||||
end
|
||||
|
||||
test "stops logs for matching message text" do
|
||||
log_event = {:info, self(), {Logger, "HEAD / responded 200", {{2026, 3, 10}, {12, 0, 0, 0}}, []}}
|
||||
|
||||
assert :stop = TelemetryFilter.filter_health_checks(log_event, [])
|
||||
end
|
||||
|
||||
test "ignores unrelated log events" do
|
||||
log_event =
|
||||
{:info, self(), {Logger, "GET /devices responded 200", {{2026, 3, 10}, {12, 0, 0, 0}}, request_path: "/devices"}}
|
||||
|
||||
assert :ignore = TelemetryFilter.filter_health_checks(log_event, [])
|
||||
end
|
||||
|
||||
test "ignores unexpected log event shapes" do
|
||||
assert :ignore = TelemetryFilter.filter_health_checks(:unexpected, [])
|
||||
end
|
||||
end
|
||||
|
||||
test "attach/0 returns ok" do
|
||||
assert :ok = TelemetryFilter.attach()
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue