towerops/test/towerops_web/live/org/integrations_live_test.exs
2026-06-15 11:15:46 -05:00

613 lines
19 KiB
Elixir

defmodule ToweropsWeb.Org.IntegrationsLiveTest do
use ToweropsWeb.ConnCase
import Phoenix.LiveViewTest
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Preseem.Client, as: PreseemClient
setup do
user = user_fixture(enable_totp: true)
organization = organization_fixture(user.id)
%{user: user, organization: organization}
end
describe "gaiia webhook setup" do
test "saving a new gaiia integration auto-generates a webhook secret", %{
conn: conn,
user: user,
organization: org
} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-gaiia") |> render_click()
view
|> form("#gaiia-form", %{integration: %{api_key: "test-gaiia-key"}})
|> render_submit()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
assert integration.credentials["api_key"] == "test-gaiia-key"
assert integration.credentials["webhook_secret"]
assert String.length(integration.credentials["webhook_secret"]) == 64
end
test "saving an existing gaiia integration preserves the webhook secret", %{
conn: conn,
user: user,
organization: org
} do
# Create integration with a known webhook secret
{:ok, _integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "old-key", "webhook_secret" => "existing-secret-abc"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-gaiia") |> render_click()
view
|> form("#gaiia-form", %{integration: %{api_key: "new-gaiia-key"}})
|> render_submit()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
assert integration.credentials["api_key"] == "old-key"
assert integration.credentials["webhook_secret"] == "existing-secret-abc"
end
test "displays webhook URL when gaiia integration is configured", %{
conn: conn,
user: user,
organization: org
} do
{:ok, _integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "/api/v1/webhooks/gaiia/#{org.id}"
end
test "displays webhook secret when gaiia integration is configured", %{
conn: conn,
user: user,
organization: org
} do
{:ok, _integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "secret123"
end
test "regenerate webhook secret changes the stored secret", %{
conn: conn,
user: user,
organization: org
} do
{:ok, _integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "key", "webhook_secret" => "old-secret"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-gaiia") |> render_click()
view |> element("#regenerate-webhook-secret") |> render_click()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
assert integration.credentials["webhook_secret"] != "old-secret"
assert String.length(integration.credentials["webhook_secret"]) == 64
assert integration.credentials["api_key"] == "key"
end
test "shows setup instructions when gaiia is being configured", %{
conn: conn,
user: user,
organization: org
} do
{:ok, _integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "key", "webhook_secret" => "secret"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "Webhook Configuration"
assert html =~ "Webhook URL"
assert html =~ "Webhook Secret"
assert html =~ "Gaiia admin panel"
end
end
describe "index" do
test "renders integrations page", %{conn: conn, user: user, organization: org} do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
assert html =~ "Integrations"
assert html =~ "Preseem"
end
test "shows preseem description", %{conn: conn, user: user, organization: org} do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
assert html =~ "QoE monitoring"
end
test "can open preseem configuration", %{conn: conn, user: user, organization: org} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
html = view |> element("#configure-preseem") |> render_click()
assert html =~ "API Key"
end
test "can save preseem credentials", %{conn: conn, user: user, organization: org} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-preseem") |> render_click()
view
|> form("#preseem-form", %{integration: %{api_key: "test-key-123"}})
|> render_submit()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "preseem")
assert integration.credentials["api_key"] == "test-key-123"
end
test "can test preseem connection successfully", %{conn: conn, user: user, organization: org} do
Req.Test.stub(PreseemClient, fn conn ->
Req.Test.json(conn, %{
"data" => [%{"id" => 1, "name" => "Test AP"}],
"paginator" => %{"page" => 1, "page_count" => 1, "limit" => 100, "total_count" => 1}
})
end)
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-preseem") |> render_click()
view
|> form("#preseem-form", %{integration: %{api_key: "test-key"}})
|> render_change()
html = view |> element("#test-connection") |> render_click()
assert html =~ "Connection successful"
end
test "can test gaiia connection (drives test_provider_connection \"gaiia\" branch)",
%{conn: conn, user: user, organization: org} do
Req.Test.stub(Towerops.Gaiia.Client, fn c ->
Req.Test.json(c, %{"status" => "ok"})
end)
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-gaiia") |> render_click()
view
|> form("#gaiia-form", %{integration: %{api_key: "k"}})
|> render_change()
_ = render_click(view, "test_connection", %{})
# Just exercises the gaiia branch — pass/fail is fine, the LV stays alive.
assert render(view) =~ "Test"
end
test "can test sonar connection (drives test_provider_connection \"sonar\" branch)",
%{conn: conn, user: user, organization: org} do
Req.Test.stub(Towerops.Sonar.Client, fn c ->
Req.Test.json(c, %{"data" => []})
end)
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-sonar") |> render_click()
view
|> form("#sonar-form", %{integration: %{instance_url: "https://x.sonar.test", api_token: "k"}})
|> render_change()
_ = render_click(view, "test_connection", %{})
assert render(view) =~ "Test"
end
test "can test splynx connection (drives test_provider_connection \"splynx\" branch)",
%{conn: conn, user: user, organization: org} do
Req.Test.stub(Towerops.Splynx.Client, fn c ->
Req.Test.json(c, %{"access_token" => "tok"})
end)
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-splynx") |> render_click()
view
|> form("#splynx-form", %{
integration: %{instance_url: "https://x.splynx.test", api_key: "k", api_secret: "s"}
})
|> render_change()
_ = render_click(view, "test_connection", %{})
assert render(view) =~ "Test"
end
test "can test visp connection (drives test_provider_connection \"visp\" branch)",
%{conn: conn, user: user, organization: org} do
Req.Test.stub(Towerops.Visp.Client, fn c ->
Req.Test.json(c, %{"data" => []})
end)
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-visp") |> render_click()
view
|> form("#visp-form", %{integration: %{api_key: "k"}})
|> render_change()
_ = render_click(view, "test_connection", %{})
assert render(view) =~ "Test"
end
test "close_config clears the configuring panel", %{conn: conn, user: user, organization: org} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# Open preseem config first
html = view |> element("#configure-preseem") |> render_click()
assert html =~ "API Key"
# Trigger close via the LV process (the close button is rendered inside the panel)
html = render_hook(view, "close_config", %{})
refute html =~ ~s(name="integration[api_key]")
end
test "validate event updates the form changeset state", %{
conn: conn,
user: user,
organization: org
} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-preseem") |> render_click()
html =
view
|> form("#preseem-form", %{integration: %{api_key: "validated-key"}})
|> render_change()
# The form re-renders without crashing — verify the configure panel is still active
assert html =~ "API Key"
end
test "test_connection without filling required fields shows error", %{
conn: conn,
user: user,
organization: org
} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-preseem") |> render_click()
html = view |> element("#test-connection") |> render_click()
assert html =~ "Please fill in all required fields first"
end
test "toggle_enabled flips an existing integration's enabled flag", %{
conn: conn,
user: user,
organization: org
} do
{:ok, integration} =
Towerops.Integrations.create_integration(org.id, %{
provider: "preseem",
enabled: true,
credentials: %{"api_key" => "key"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
render_hook(view, "toggle_enabled", %{"provider" => "preseem"})
{:ok, reloaded} = Towerops.Integrations.get_integration(org.id, "preseem")
refute reloaded.enabled
_ = integration
end
test "toggle_enabled is a no-op for unknown integration", %{
conn: conn,
user: user,
organization: org
} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# Should not crash even though no integration exists for "preseem"
html = render_hook(view, "toggle_enabled", %{"provider" => "preseem"})
assert is_binary(html) and byte_size(html) > 0
end
test "saving exclusive billing integration when another active blocks save", %{
conn: conn,
user: user,
organization: org
} do
# Activate gaiia first
{:ok, _} =
Towerops.Integrations.create_integration(org.id, %{
provider: "gaiia",
enabled: true,
credentials: %{"api_key" => "k", "webhook_secret" => "s"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# When active billing exists, the picker for sonar isn't rendered, so
# drive the events directly through the LV.
render_hook(view, "configure", %{"provider" => "sonar"})
html =
render_hook(view, "save", %{
"integration" => %{"instance_url" => "https://x", "api_token" => "t"}
})
assert html =~ "Only one"
end
test "regenerate_webhook_secret is a no-op when no gaiia integration exists", %{
conn: conn,
user: user,
organization: org
} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# No gaiia integration; event should be a no-op (and not crash).
html = render_hook(view, "regenerate_webhook_secret", %{})
assert is_binary(html) and byte_size(html) > 0
end
test "renders provider category descriptions", %{conn: conn, user: user, organization: org} do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
assert html =~ "Billing"
assert html =~ "Incident"
assert html =~ "Infrastructure"
end
test "renders next_sync_minutes and humanize_interval for integration with last_synced_at",
%{conn: conn, user: user, organization: org} do
now = DateTime.truncate(DateTime.utc_now(), :second)
{:ok, _} =
Towerops.Integrations.create_integration(org.id, %{
provider: "preseem",
enabled: true,
credentials: %{"api_key" => "k"},
last_sync_status: "success",
last_synced_at: now,
sync_interval_minutes: 90
})
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# humanize_interval(90) -> "1h 30min"
assert html =~ "1h 30min"
# Next sync should appear
assert html =~ ~r/Next in|Syncs every/
end
test "renders humanize_interval at exact hour boundary",
%{conn: conn, user: user, organization: org} do
{:ok, _} =
Towerops.Integrations.create_integration(org.id, %{
provider: "preseem",
enabled: true,
credentials: %{"api_key" => "k"},
last_sync_status: "success",
last_synced_at: DateTime.truncate(DateTime.utc_now(), :second),
sync_interval_minutes: 120
})
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
# humanize_interval(120) -> "2h"
assert html =~ "2h"
end
test "renders humanize_interval below hour", %{conn: conn, user: user, organization: org} do
{:ok, _} =
Towerops.Integrations.create_integration(org.id, %{
provider: "preseem",
enabled: true,
credentials: %{"api_key" => "k"},
last_sync_status: "success",
last_synced_at: DateTime.truncate(DateTime.utc_now(), :second),
sync_interval_minutes: 30
})
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
assert html =~ "30min"
end
test "test_connection without filling required fields shows error for cn_maestro",
%{conn: conn, user: user, organization: org} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-cn_maestro") |> render_click()
html = render_hook(view, "test_connection", %{})
assert html =~ "Please fill in all required fields first"
end
test "test_connection without filling required fields shows error for uisp",
%{conn: conn, user: user, organization: org} do
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-uisp") |> render_click()
html = render_hook(view, "test_connection", %{})
assert html =~ "Please fill in all required fields first"
end
test "toggle_enabled is rejected when an exclusive integration is already active",
%{conn: conn, user: user, organization: org} do
# Sonar (billing exclusive) is enabled
{:ok, _sonar} =
Towerops.Integrations.create_integration(org.id, %{
provider: "sonar",
enabled: true,
credentials: %{"instance_url" => "https://x", "api_token" => "t"}
})
# Splynx (also billing exclusive) is disabled
{:ok, _splynx} =
Towerops.Integrations.create_integration(org.id, %{
provider: "splynx",
enabled: false,
credentials: %{"instance_url" => "https://y", "api_key" => "k", "api_secret" => "s"}
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
html = render_hook(view, "toggle_enabled", %{"provider" => "splynx"})
# Should hit the exclusive_conflict? branch
assert html =~ ~r/Only one|Disable/
end
test "clears sync status when credentials change", %{conn: conn, user: user, organization: org} do
{:ok, _} =
Towerops.Integrations.create_integration(org.id, %{
provider: "preseem",
enabled: true,
credentials: %{"api_key" => "old-key"},
last_sync_status: "failed",
last_sync_message: "some raw error from previous sync"
})
{:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
view |> element("#configure-preseem") |> render_click()
view
|> form("#preseem-form", %{integration: %{api_key: "new-key-456"}})
|> render_submit()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "preseem")
assert integration.credentials["api_key"] == "new-key-456"
assert integration.last_sync_status == "never"
assert is_nil(integration.last_sync_message)
assert is_nil(integration.last_synced_at)
end
end
end