613 lines
19 KiB
Elixir
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
|