Add comprehensive test coverage for LiveView modules and contexts
- Add LiveView tests for DashboardLive, AlertLive, SiteLive, EquipmentLive, OrgLive - Add comprehensive tests for Organizations context (memberships, invitations, auth) - Add tests for AlertNotifier email delivery - Improve test coverage from 54.78% to 86.65% - Add 101 new tests (256 total tests)
This commit is contained in:
parent
c27aa4f5f8
commit
b0a17e7ea6
15 changed files with 1713 additions and 146 deletions
|
|
@ -77,11 +77,14 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
/>
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p :if={@title} class={[
|
||||
"text-sm font-medium",
|
||||
@kind == :info && "text-blue-900 dark:text-blue-100",
|
||||
@kind == :error && "text-red-900 dark:text-red-100"
|
||||
]}>
|
||||
<p
|
||||
:if={@title}
|
||||
class={[
|
||||
"text-sm font-medium",
|
||||
@kind == :info && "text-blue-900 dark:text-blue-100",
|
||||
@kind == :error && "text-red-900 dark:text-red-100"
|
||||
]}
|
||||
>
|
||||
{@title}
|
||||
</p>
|
||||
<p class={[
|
||||
|
|
@ -98,8 +101,10 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
type="button"
|
||||
class={[
|
||||
"inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2",
|
||||
@kind == :info && "text-blue-500 hover:text-blue-600 focus:ring-blue-500 dark:text-blue-400",
|
||||
@kind == :error && "text-red-500 hover:text-red-600 focus:ring-red-500 dark:text-red-400"
|
||||
@kind == :info &&
|
||||
"text-blue-500 hover:text-blue-600 focus:ring-blue-500 dark:text-blue-400",
|
||||
@kind == :error &&
|
||||
"text-red-500 hover:text-red-600 focus:ring-red-500 dark:text-red-400"
|
||||
]}
|
||||
aria-label={gettext("close")}
|
||||
>
|
||||
|
|
@ -130,8 +135,7 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
|
||||
def button(%{rest: rest} = assigns) do
|
||||
variants = %{
|
||||
"primary" =>
|
||||
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600",
|
||||
"primary" => "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600",
|
||||
nil =>
|
||||
"bg-white text-zinc-900 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 focus:ring-blue-500 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-zinc-700 dark:hover:bg-zinc-700"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,16 @@
|
|||
class="mt-6 rounded-lg bg-blue-50 p-4 dark:bg-blue-950"
|
||||
>
|
||||
<div class="flex">
|
||||
<.icon name="hero-information-circle" class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-200">
|
||||
You are running the local mail adapter.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-200">
|
||||
To see sent emails, visit
|
||||
<.link href="/dev/mailbox" class="font-medium underline">the mailbox page</.link>.
|
||||
To see sent emails, visit <.link href="/dev/mailbox" class="font-medium underline">the mailbox page</.link>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -74,7 +76,12 @@
|
|||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
<.input field={f[:password]} type="password" label="Password" autocomplete="current-password" />
|
||||
<.input
|
||||
field={f[:password]}
|
||||
type="password"
|
||||
label="Password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<div class="space-y-3">
|
||||
<.button class="w-full" variant="primary" name={@form[:remember_me].name} value="true">
|
||||
Log in and stay logged in <span aria-hidden="true">→</span>
|
||||
|
|
|
|||
|
|
@ -68,11 +68,9 @@
|
|||
]}>
|
||||
<%= case alert.alert_type do %>
|
||||
<% :equipment_down -> %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
|
||||
Equipment Down
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" /> Equipment Down
|
||||
<% :equipment_up -> %>
|
||||
<.icon name="hero-check-circle" class="h-4 w-4" />
|
||||
Equipment Recovered
|
||||
<.icon name="hero-check-circle" class="h-4 w-4" /> Equipment Recovered
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
|
|
@ -91,7 +89,9 @@
|
|||
|
||||
<h3 class="mt-3 font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"}
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"
|
||||
}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
|
|
@ -126,7 +126,9 @@
|
|||
<div>
|
||||
<strong class="font-medium">Site:</strong>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{alert.equipment.site.id}"}
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/sites/#{alert.equipment.site.id}"
|
||||
}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.site.name}
|
||||
|
|
|
|||
|
|
@ -4,140 +4,142 @@
|
|||
<:subtitle>Welcome to {@current_organization.name}</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Sites</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@sites_count}</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Total sites</p>
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Equipment</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment_count}</p>
|
||||
<div class="mt-3 space-y-1.5 text-sm">
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{@equipment_up} Up
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{@equipment_down} Down
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-zinc-400"></span>
|
||||
{@equipment_unknown} Unknown
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Active Alerts</h3>
|
||||
<p class={[
|
||||
"mt-2 text-3xl font-bold",
|
||||
(length(@active_alerts) > 0 && "text-red-600 dark:text-red-500") ||
|
||||
"text-zinc-900 dark:text-zinc-100"
|
||||
]}>
|
||||
{length(@active_alerts)}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Requires attention</p>
|
||||
</.link>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">System Status</h3>
|
||||
<div class={[
|
||||
"mt-3 flex items-center gap-2 text-sm font-semibold",
|
||||
(@equipment_down == 0 && "text-green-600 dark:text-green-500") ||
|
||||
"text-amber-600 dark:text-amber-500"
|
||||
]}>
|
||||
<%= if @equipment_down == 0 do %>
|
||||
<.icon name="hero-check-circle" class="h-5 w-5" />
|
||||
<span>All Systems Operational</span>
|
||||
<% else %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
||||
<span>{@equipment_down} Equipment Down</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if length(@active_alerts) > 0 do %>
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">Active Alerts</h2>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
View All Alerts →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<%= for alert <- Enum.take(@active_alerts, 5) do %>
|
||||
<div class="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-900/50 dark:bg-red-950/30">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-500"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
</.link>
|
||||
</h3>
|
||||
<p class="mt-0.5 text-sm text-zinc-700 dark:text-zinc-300">{alert.message}</p>
|
||||
</div>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400 whitespace-nowrap">
|
||||
{Calendar.strftime(alert.triggered_at, "%H:%M")}
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if length(@active_alerts) > 5 do %>
|
||||
<div class="text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
+ {length(@active_alerts) - 5} more alerts
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-zinc-900 dark:text-zinc-100">Quick Actions</h2>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites"}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<.icon name="hero-building-office" class="h-5 w-5" />
|
||||
<span>Manage Sites</span>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Sites</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@sites_count}</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Total sites</p>
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<.icon name="hero-server" class="h-5 w-5" />
|
||||
<span>Manage Equipment</span>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Equipment</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment_count}</p>
|
||||
<div class="mt-3 space-y-1.5 text-sm">
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{@equipment_up} Up
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{@equipment_down} Down
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-zinc-700 dark:text-zinc-300">
|
||||
<span class="h-2 w-2 rounded-full bg-zinc-400"></span>
|
||||
{@equipment_unknown} Unknown
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2.5 text-sm font-semibold text-zinc-700 shadow-sm hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<.icon name="hero-bell" class="h-5 w-5" />
|
||||
<span>View Alerts</span>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Active Alerts</h3>
|
||||
<p class={[
|
||||
"mt-2 text-3xl font-bold",
|
||||
(length(@active_alerts) > 0 && "text-red-600 dark:text-red-500") ||
|
||||
"text-zinc-900 dark:text-zinc-100"
|
||||
]}>
|
||||
{length(@active_alerts)}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Requires attention</p>
|
||||
</.link>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">System Status</h3>
|
||||
<div class={[
|
||||
"mt-3 flex items-center gap-2 text-sm font-semibold",
|
||||
(@equipment_down == 0 && "text-green-600 dark:text-green-500") ||
|
||||
"text-amber-600 dark:text-amber-500"
|
||||
]}>
|
||||
<%= if @equipment_down == 0 do %>
|
||||
<.icon name="hero-check-circle" class="h-5 w-5" />
|
||||
<span>All Systems Operational</span>
|
||||
<% else %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
||||
<span>{@equipment_down} Equipment Down</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if length(@active_alerts) > 0 do %>
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">Active Alerts</h2>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
View All Alerts →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<%= for alert <- Enum.take(@active_alerts, 5) do %>
|
||||
<div class="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-900/50 dark:bg-red-950/30">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-500"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"
|
||||
}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
</.link>
|
||||
</h3>
|
||||
<p class="mt-0.5 text-sm text-zinc-700 dark:text-zinc-300">{alert.message}</p>
|
||||
</div>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400 whitespace-nowrap">
|
||||
{Calendar.strftime(alert.triggered_at, "%H:%M")}
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if length(@active_alerts) > 5 do %>
|
||||
<div class="text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
+ {length(@active_alerts) - 5} more alerts
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-zinc-900 dark:text-zinc-100">Quick Actions</h2>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites"}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
<.icon name="hero-building-office" class="h-5 w-5" />
|
||||
<span>Manage Sites</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
<.icon name="hero-server" class="h-5 w-5" />
|
||||
<span>Manage Equipment</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2.5 text-sm font-semibold text-zinc-700 shadow-sm hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
|
||||
>
|
||||
<.icon name="hero-bell" class="h-5 w-5" />
|
||||
<span>View Alerts</span>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>
|
||||
{if @live_action == :new, do: "Add new equipment to monitor", else: "Update equipment details"}
|
||||
{if @live_action == :new,
|
||||
do: "Add new equipment to monitor",
|
||||
else: "Update equipment details"}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@
|
|||
Get started by adding your first equipment.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"} variant="primary">
|
||||
<.button
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"}
|
||||
variant="primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Equipment
|
||||
</.button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,8 @@
|
|||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
check.status == :success &&
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
check.status == :failure && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
check.status == :failure &&
|
||||
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
]}>
|
||||
{check.status |> to_string() |> String.upcase()}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@
|
|||
|
||||
<%= if @sites == [] do %>
|
||||
<div class="text-center py-16">
|
||||
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<.icon
|
||||
name="hero-building-office"
|
||||
class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600"
|
||||
/>
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No sites</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Get started by creating your first site.
|
||||
|
|
|
|||
167
test/towerops/alerts/alert_notifier_test.exs
Normal file
167
test/towerops/alerts/alert_notifier_test.exs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
defmodule Towerops.Alerts.AlertNotifierTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
import Swoosh.TestAssertions
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Alerts.AlertNotifier
|
||||
|
||||
setup do
|
||||
owner = user_fixture()
|
||||
admin = user_fixture()
|
||||
member = user_fixture()
|
||||
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Test Org"}, owner.id)
|
||||
|
||||
# Add admin and member
|
||||
{:ok, _admin_membership} =
|
||||
Towerops.Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: admin.id,
|
||||
role: :admin
|
||||
})
|
||||
|
||||
{:ok, _member_membership} =
|
||||
Towerops.Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: member.id,
|
||||
role: :member
|
||||
})
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{owner: owner, admin: admin, member: member, organization: organization, equipment: equipment}
|
||||
end
|
||||
|
||||
describe "deliver_alert_notification/1" do
|
||||
test "sends equipment_down alert to owners and admins", %{
|
||||
owner: owner,
|
||||
admin: admin,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
# Should send to 2 recipients (owner and admin, not member)
|
||||
assert length(results) == 2
|
||||
|
||||
# Verify emails were sent to owner and admin
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}")
|
||||
assert_email_sent(to: owner.email)
|
||||
assert_email_sent(to: admin.email)
|
||||
end
|
||||
|
||||
test "sends equipment_up alert to owners and admins", %{
|
||||
owner: owner,
|
||||
admin: admin,
|
||||
equipment: equipment,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
})
|
||||
|
||||
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
# Should send to 2 recipients (owner and admin)
|
||||
assert length(results) == 2
|
||||
|
||||
# Verify emails were sent
|
||||
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}")
|
||||
assert_email_sent(to: owner.email)
|
||||
assert_email_sent(to: admin.email)
|
||||
end
|
||||
|
||||
test "equipment_down alert includes correct information", %{
|
||||
owner: owner,
|
||||
equipment: equipment,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.to == [{nil, owner.email}] &&
|
||||
email.subject =~ "Equipment Down" &&
|
||||
email.text_body =~ organization.name &&
|
||||
email.text_body =~ equipment.name &&
|
||||
email.text_body =~ equipment.ip_address &&
|
||||
email.text_body =~ "Test Site" &&
|
||||
email.text_body =~ "not responding to ping checks"
|
||||
end)
|
||||
end
|
||||
|
||||
test "equipment_up alert includes correct information", %{
|
||||
owner: owner,
|
||||
equipment: equipment,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
})
|
||||
|
||||
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.to == [{nil, owner.email}] &&
|
||||
email.subject =~ "Equipment Recovered" &&
|
||||
email.text_body =~ organization.name &&
|
||||
email.text_body =~ equipment.name &&
|
||||
email.text_body =~ equipment.ip_address &&
|
||||
email.text_body =~ "Test Site" &&
|
||||
email.text_body =~ "now responding to ping checks"
|
||||
end)
|
||||
end
|
||||
|
||||
test "uses correct from address", %{equipment: equipment} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.from == {"TowerOps Alerts", "alerts@towerops.example.com"}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
394
test/towerops/organizations_test.exs
Normal file
394
test/towerops/organizations_test.exs
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
defmodule Towerops.OrganizationsTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Organizations
|
||||
|
||||
describe "organizations" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
test "list_user_organizations/1 returns all organizations for a user", %{user: user} do
|
||||
{:ok, org1} = Organizations.create_organization(%{name: "Org 1"}, user.id)
|
||||
{:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, user.id)
|
||||
|
||||
orgs = Organizations.list_user_organizations(user.id)
|
||||
assert length(orgs) == 2
|
||||
org_ids = Enum.map(orgs, & &1.id)
|
||||
assert org1.id in org_ids
|
||||
assert org2.id in org_ids
|
||||
end
|
||||
|
||||
test "list_user_organizations/1 returns empty list for user with no orgs" do
|
||||
user = user_fixture()
|
||||
assert Organizations.list_user_organizations(user.id) == []
|
||||
end
|
||||
|
||||
test "get_organization!/1 returns the organization with given id", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
assert Organizations.get_organization!(organization.id).id == organization.id
|
||||
end
|
||||
|
||||
test "get_organization_by_slug!/1 returns the organization with given slug", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
assert Organizations.get_organization_by_slug!(organization.slug).id == organization.id
|
||||
end
|
||||
|
||||
test "create_organization/2 with valid data creates organization", %{user: user} do
|
||||
valid_attrs = %{name: "New Organization"}
|
||||
|
||||
assert {:ok, organization} = Organizations.create_organization(valid_attrs, user.id)
|
||||
assert organization.name == "New Organization"
|
||||
assert organization.slug
|
||||
end
|
||||
|
||||
test "create_organization/2 creates owner membership", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
membership = Organizations.get_membership(organization.id, user.id)
|
||||
assert membership.role == :owner
|
||||
end
|
||||
|
||||
test "create_organization/2 with invalid data returns error changeset" do
|
||||
user = user_fixture()
|
||||
assert {:error, changeset} = Organizations.create_organization(%{name: nil}, user.id)
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "update_organization/2 with valid data updates the organization", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
update_attrs = %{name: "Updated Org"}
|
||||
assert {:ok, updated} = Organizations.update_organization(organization, update_attrs)
|
||||
assert updated.name == "Updated Org"
|
||||
end
|
||||
|
||||
test "update_organization/2 with invalid data returns error changeset", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
assert {:error, changeset} = Organizations.update_organization(organization, %{name: nil})
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "delete_organization/1 deletes the organization", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
assert {:ok, _} = Organizations.delete_organization(organization)
|
||||
assert_raise Ecto.NoResultsError, fn -> Organizations.get_organization!(organization.id) end
|
||||
end
|
||||
|
||||
test "change_organization/1 returns an organization changeset", %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
assert %Ecto.Changeset{} = Organizations.change_organization(organization)
|
||||
end
|
||||
end
|
||||
|
||||
describe "memberships" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
%{user: user, organization: organization}
|
||||
end
|
||||
|
||||
test "get_membership/2 returns the membership", %{user: user, organization: organization} do
|
||||
membership = Organizations.get_membership(organization.id, user.id)
|
||||
assert membership.user_id == user.id
|
||||
assert membership.organization_id == organization.id
|
||||
end
|
||||
|
||||
test "get_membership/2 returns nil when membership doesn't exist", %{organization: organization} do
|
||||
other_user = user_fixture()
|
||||
assert Organizations.get_membership(organization.id, other_user.id) == nil
|
||||
end
|
||||
|
||||
test "get_membership!/2 returns the membership", %{user: user, organization: organization} do
|
||||
membership = Organizations.get_membership!(organization.id, user.id)
|
||||
assert membership.user_id == user.id
|
||||
end
|
||||
|
||||
test "get_membership!/2 raises when membership doesn't exist", %{organization: organization} do
|
||||
other_user = user_fixture()
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Organizations.get_membership!(organization.id, other_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
test "list_organization_memberships/1 returns all memberships", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
# Add another member
|
||||
other_user = user_fixture()
|
||||
|
||||
{:ok, _membership} =
|
||||
Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: other_user.id,
|
||||
role: :member
|
||||
})
|
||||
|
||||
memberships = Organizations.list_organization_memberships(organization.id)
|
||||
assert length(memberships) == 2
|
||||
user_ids = Enum.map(memberships, & &1.user_id)
|
||||
assert user.id in user_ids
|
||||
assert other_user.id in user_ids
|
||||
end
|
||||
|
||||
test "list_organization_notification_recipients/1 returns owners and admins", %{
|
||||
organization: organization
|
||||
} do
|
||||
admin = user_fixture()
|
||||
member = user_fixture()
|
||||
|
||||
{:ok, _admin_membership} =
|
||||
Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: admin.id,
|
||||
role: :admin
|
||||
})
|
||||
|
||||
{:ok, _member_membership} =
|
||||
Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: member.id,
|
||||
role: :member
|
||||
})
|
||||
|
||||
recipients = Organizations.list_organization_notification_recipients(organization.id)
|
||||
recipient_ids = Enum.map(recipients, & &1.id)
|
||||
|
||||
# Owner and admin should be in recipients
|
||||
assert admin.id in recipient_ids
|
||||
# Member should NOT be in recipients
|
||||
refute member.id in recipient_ids
|
||||
end
|
||||
|
||||
test "create_membership/1 with valid data creates membership", %{organization: organization} do
|
||||
new_user = user_fixture()
|
||||
|
||||
valid_attrs = %{
|
||||
organization_id: organization.id,
|
||||
user_id: new_user.id,
|
||||
role: :member
|
||||
}
|
||||
|
||||
assert {:ok, membership} = Organizations.create_membership(valid_attrs)
|
||||
assert membership.role == :member
|
||||
assert membership.user_id == new_user.id
|
||||
end
|
||||
|
||||
test "create_membership/1 with invalid data returns error changeset" do
|
||||
assert {:error, changeset} = Organizations.create_membership(%{})
|
||||
assert "can't be blank" in errors_on(changeset).organization_id
|
||||
end
|
||||
|
||||
test "update_membership/2 changes user role", %{user: user, organization: organization} do
|
||||
membership = Organizations.get_membership!(organization.id, user.id)
|
||||
|
||||
assert {:ok, updated} = Organizations.update_membership(membership, %{role: :admin})
|
||||
assert updated.role == :admin
|
||||
end
|
||||
|
||||
test "delete_membership/1 deletes the membership", %{organization: organization} do
|
||||
new_user = user_fixture()
|
||||
|
||||
{:ok, membership} =
|
||||
Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: new_user.id,
|
||||
role: :member
|
||||
})
|
||||
|
||||
assert {:ok, _} = Organizations.delete_membership(membership)
|
||||
assert Organizations.get_membership(organization.id, new_user.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "invitations" do
|
||||
setup do
|
||||
owner = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, owner.id)
|
||||
%{owner: owner, organization: organization}
|
||||
end
|
||||
|
||||
test "list_pending_invitations/1 returns pending invitations", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "invited@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: "test-token-#{System.unique_integer()}",
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
})
|
||||
|
||||
invitations = Organizations.list_pending_invitations(organization.id)
|
||||
assert length(invitations) == 1
|
||||
assert hd(invitations).id == invitation.id
|
||||
end
|
||||
|
||||
test "list_pending_invitations/1 excludes accepted invitations", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "invited@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: "test-token-#{System.unique_integer()}",
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day),
|
||||
accepted_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
invitations = Organizations.list_pending_invitations(organization.id)
|
||||
refute Enum.any?(invitations, &(&1.id == invitation.id))
|
||||
end
|
||||
|
||||
test "list_pending_invitations/1 excludes expired invitations", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "invited@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: "test-token-#{System.unique_integer()}",
|
||||
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
||||
})
|
||||
|
||||
invitations = Organizations.list_pending_invitations(organization.id)
|
||||
refute Enum.any?(invitations, &(&1.id == invitation.id))
|
||||
end
|
||||
|
||||
test "create_invitation/1 with valid data creates invitation", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
valid_attrs = %{
|
||||
organization_id: organization.id,
|
||||
email: "test@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: "unique-token-#{System.unique_integer()}",
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
}
|
||||
|
||||
assert {:ok, invitation} = Organizations.create_invitation(valid_attrs)
|
||||
assert invitation.email == "test@example.com"
|
||||
assert invitation.role == :member
|
||||
end
|
||||
|
||||
test "get_invitation_by_token/1 returns invitation", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
token = "test-token-#{System.unique_integer()}"
|
||||
|
||||
{:ok, _invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "test@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: token,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
})
|
||||
|
||||
invitation = Organizations.get_invitation_by_token(token)
|
||||
assert invitation.token == token
|
||||
end
|
||||
|
||||
test "get_invitation_by_token/1 returns nil for accepted invitation", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
token = "test-token-#{System.unique_integer()}"
|
||||
|
||||
{:ok, _invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "test@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: token,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day),
|
||||
accepted_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
assert Organizations.get_invitation_by_token(token) == nil
|
||||
end
|
||||
|
||||
test "accept_invitation/2 creates membership and marks invitation accepted", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
token = "test-token-#{System.unique_integer()}"
|
||||
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "test@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: token,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
})
|
||||
|
||||
new_user = user_fixture()
|
||||
assert {:ok, membership} = Organizations.accept_invitation(invitation, new_user.id)
|
||||
assert membership.user_id == new_user.id
|
||||
assert membership.role == :member
|
||||
|
||||
# Invitation should no longer be retrievable
|
||||
assert Organizations.get_invitation_by_token(token) == nil
|
||||
end
|
||||
|
||||
test "delete_invitation/1 deletes the invitation", %{
|
||||
owner: owner,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: "test@example.com",
|
||||
role: :member,
|
||||
invited_by_id: owner.id,
|
||||
token: "test-token-#{System.unique_integer()}",
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
})
|
||||
|
||||
assert {:ok, _} = Organizations.delete_invitation(invitation)
|
||||
assert Organizations.list_pending_invitations(organization.id) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorization" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
membership = Organizations.get_membership!(organization.id, user.id)
|
||||
%{membership: membership}
|
||||
end
|
||||
|
||||
test "can?/3 delegates to Policy module", %{membership: membership} do
|
||||
# Owner can delete organization
|
||||
assert Organizations.can?(membership, :delete, :organization)
|
||||
|
||||
# Change to member role
|
||||
{:ok, member_membership} = Organizations.update_membership(membership, %{role: :member})
|
||||
|
||||
# Member cannot delete organization
|
||||
refute Organizations.can?(member_membership, :delete, :organization)
|
||||
end
|
||||
end
|
||||
end
|
||||
199
test/towerops_web/live/alert_live_test.exs
Normal file
199
test/towerops_web/live/alert_live_test.exs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
defmodule ToweropsWeb.AlertLive.IndexTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, equipment: equipment}
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
test "displays alerts page", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert html =~ "Alerts"
|
||||
assert html =~ "Monitor and acknowledge system alerts"
|
||||
end
|
||||
|
||||
test "displays empty state when no alerts", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert html =~ "No alerts"
|
||||
end
|
||||
|
||||
test "lists all alerts", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert html =~ "Test Router"
|
||||
assert html =~ "Equipment is down"
|
||||
assert html =~ "Equipment Down"
|
||||
end
|
||||
|
||||
test "filters active alerts", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
# Create an active equipment_down alert
|
||||
{:ok, _active_alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
# Create a recovery alert (equipment_up)
|
||||
{:ok, _recovery_alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
})
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/alerts?filter=active")
|
||||
|
||||
# Should show equipment_down alert
|
||||
assert html =~ "Equipment is down"
|
||||
# Should NOT show equipment_up alert in active filter
|
||||
refute html =~ "Equipment recovered"
|
||||
end
|
||||
|
||||
test "acknowledges an alert", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button", "Acknowledge")
|
||||
|> render_click(%{"id" => alert.id})
|
||||
|
||||
assert html =~ "Alert acknowledged"
|
||||
end
|
||||
|
||||
test "does not show acknowledge button for equipment_up alerts", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment recovered"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
# Should not show acknowledge button for equipment_up alerts
|
||||
refute html =~ "Acknowledge"
|
||||
end
|
||||
|
||||
test "does not show acknowledge button for resolved alerts", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
# Resolve the alert
|
||||
Towerops.Alerts.resolve_alert(alert)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert html =~ "Resolved"
|
||||
refute html =~ "Acknowledge"
|
||||
end
|
||||
|
||||
test "updates in real-time when new alert is created", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
# Broadcast new alert event
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
assert render(view)
|
||||
end
|
||||
|
||||
test "updates in real-time when alert is resolved", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
# Resolve the alert and broadcast
|
||||
Towerops.Alerts.resolve_alert(alert)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, equipment.id, :equipment_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
assert render(view)
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
133
test/towerops_web/live/dashboard_live_test.exs
Normal file
133
test/towerops_web/live/dashboard_live_test.exs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
defmodule ToweropsWeb.DashboardLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site}
|
||||
end
|
||||
|
||||
describe "Dashboard" do
|
||||
test "displays organization name and stats", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
||||
assert html =~ organization.name
|
||||
assert html =~ "Sites"
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ "Active Alerts"
|
||||
end
|
||||
|
||||
test "displays correct site count", %{conn: conn, organization: organization} do
|
||||
# Create additional sites
|
||||
{:ok, _site2} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Site 2",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
assert html =~ "Sites"
|
||||
end
|
||||
|
||||
test "displays correct equipment counts", %{conn: conn, organization: organization, site: site} do
|
||||
# Create equipment with different statuses
|
||||
{:ok, equipment_up} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
Towerops.Equipment.update_equipment_status(equipment_up, :up)
|
||||
|
||||
{:ok, equipment_down} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 2",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true
|
||||
})
|
||||
|
||||
Towerops.Equipment.update_equipment_status(equipment_down, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ "UP"
|
||||
assert html =~ "DOWN"
|
||||
end
|
||||
|
||||
test "displays active alerts count", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
# Create an active alert
|
||||
{:ok, _alert} =
|
||||
Towerops.Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Equipment is down"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
assert html =~ "Active Alerts"
|
||||
end
|
||||
|
||||
test "updates in real-time when new alert is triggered", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
||||
# Trigger a new alert via PubSub
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
)
|
||||
|
||||
# View should update
|
||||
assert render(view)
|
||||
end
|
||||
|
||||
test "stat cards are clickable links", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
||||
# Check for navigation links
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/sites"
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/equipment"
|
||||
assert html =~ ~p"/orgs/#{organization.slug}/alerts"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
296
test/towerops_web/live/equipment_live_test.exs
Normal file
296
test/towerops_web/live/equipment_live_test.exs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
defmodule ToweropsWeb.EquipmentLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site}
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
test "lists all equipment", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert html =~ "Equipment"
|
||||
assert html =~ equipment.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "displays empty state when no equipment", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert html =~ "No equipment"
|
||||
end
|
||||
|
||||
test "has link to add new equipment", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Show" do
|
||||
setup %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
description: "Main router",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 300
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
end
|
||||
|
||||
test "displays equipment details", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ "192.168.1.1"
|
||||
assert html =~ "Main router"
|
||||
end
|
||||
|
||||
test "displays monitoring status", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ "Monitoring"
|
||||
assert html =~ "Enabled"
|
||||
assert html =~ "300 seconds"
|
||||
end
|
||||
|
||||
test "displays site link", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ "Test Site"
|
||||
end
|
||||
|
||||
test "displays recent checks", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
# Create a check
|
||||
{:ok, _check} =
|
||||
Towerops.Monitoring.create_check(%{
|
||||
equipment_id: equipment.id,
|
||||
status: :success,
|
||||
response_time_ms: 10,
|
||||
checked_at: DateTime.utc_now()
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ "Recent Checks"
|
||||
assert html =~ "10ms"
|
||||
end
|
||||
|
||||
test "displays empty state when no checks", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ "No monitoring checks yet"
|
||||
end
|
||||
|
||||
test "triggers manual check", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
equipment: equipment
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button", "Check Now")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Check triggered"
|
||||
end
|
||||
|
||||
test "deletes equipment", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> element("button", "Delete")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert html =~ "Equipment deleted successfully"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, equipment: equipment} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "New" do
|
||||
test "renders new equipment form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
assert html =~ "Add new equipment to monitor"
|
||||
end
|
||||
|
||||
test "creates new equipment", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
name: "New Router",
|
||||
ip_address: "192.168.1.100",
|
||||
site_id: site.id,
|
||||
description: "Test router",
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 300
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment")
|
||||
|
||||
assert html =~ "Equipment created successfully"
|
||||
assert html =~ "New Router"
|
||||
end
|
||||
|
||||
test "validates required fields", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form", equipment: %{name: "", ip_address: ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
end
|
||||
|
||||
test "validates IP address format", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
name: "Router",
|
||||
ip_address: "invalid-ip",
|
||||
site_id: site.id
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "must be a valid IPv4 or IPv6 address"
|
||||
end
|
||||
|
||||
test "pre-selects site from query param", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/new?site_id=#{site.id}")
|
||||
|
||||
assert html =~ "New Equipment"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Edit" do
|
||||
setup %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
%{equipment: equipment}
|
||||
end
|
||||
|
||||
test "renders edit form", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
|
||||
assert html =~ "Edit Equipment"
|
||||
assert html =~ "Update equipment details"
|
||||
end
|
||||
|
||||
test "updates equipment", %{conn: conn, organization: organization, equipment: equipment} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#equipment-form",
|
||||
equipment: %{
|
||||
name: "Updated Router",
|
||||
ip_address: "192.168.1.2"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}")
|
||||
|
||||
assert html =~ "Equipment updated successfully"
|
||||
assert html =~ "Updated Router"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, equipment: equipment} do
|
||||
conn = build_conn()
|
||||
|
||||
{:error, redirect} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/equipment/#{equipment.id}/edit")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
116
test/towerops_web/live/org_live_test.exs
Normal file
116
test/towerops_web/live/org_live_test.exs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
defmodule ToweropsWeb.OrgLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
describe "Index" do
|
||||
test "lists all user organizations", %{conn: conn, user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs")
|
||||
|
||||
assert html =~ "Organizations"
|
||||
assert html =~ organization.name
|
||||
end
|
||||
|
||||
test "displays empty state when user has no organizations", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs")
|
||||
|
||||
assert html =~ "No organizations"
|
||||
end
|
||||
|
||||
test "has link to create new organization", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs")
|
||||
|
||||
assert html =~ "New Organization"
|
||||
end
|
||||
|
||||
test "navigates to organization dashboard when clicking open button", %{conn: conn, user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> element("a", "Open")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}")
|
||||
|
||||
assert html =~ organization.name
|
||||
end
|
||||
|
||||
test "requires authentication" do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "New" do
|
||||
test "renders new organization form", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/new")
|
||||
|
||||
assert html =~ "New Organization"
|
||||
end
|
||||
|
||||
test "creates new organization", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/new")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#organization-form",
|
||||
organization: %{
|
||||
name: "New Organization"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs")
|
||||
|
||||
assert html =~ "Organization created successfully"
|
||||
assert html =~ "New Organization"
|
||||
end
|
||||
|
||||
test "validates required fields", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#organization-form", organization: %{name: ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
end
|
||||
|
||||
test "validates unique organization name", %{conn: conn, user: user} do
|
||||
{:ok, _organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Existing Org"}, user.id)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#organization-form",
|
||||
organization: %{
|
||||
name: "Existing Org"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "requires authentication" do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/new")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
238
test/towerops_web/live/site_live_test.exs
Normal file
238
test/towerops_web/live/site_live_test.exs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
defmodule ToweropsWeb.SiteLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
test "lists all sites", %{conn: conn, organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
location: "Building A",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert html =~ "Sites"
|
||||
assert html =~ site.name
|
||||
assert html =~ "Building A"
|
||||
end
|
||||
|
||||
test "displays empty state when no sites", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert html =~ "No sites"
|
||||
end
|
||||
|
||||
test "has link to create new site", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert html =~ "New Site"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Show" do
|
||||
setup %{organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
location: "Building A",
|
||||
description: "Main site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{site: site}
|
||||
end
|
||||
|
||||
test "displays site details", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ site.name
|
||||
assert html =~ "Building A"
|
||||
assert html =~ "Main site"
|
||||
end
|
||||
|
||||
test "displays equipment at site", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, equipment} =
|
||||
Towerops.Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ equipment.name
|
||||
assert html =~ "192.168.1.1"
|
||||
end
|
||||
|
||||
test "displays empty state when no equipment", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ "No equipment at this site yet"
|
||||
end
|
||||
|
||||
test "displays parent site link", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, parent_site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Parent Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, updated_site} =
|
||||
Towerops.Sites.update_site(site, %{parent_site_id: parent_site.id})
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/orgs/#{organization.slug}/sites/#{updated_site.id}")
|
||||
|
||||
assert html =~ "Parent Site"
|
||||
assert html =~ parent_site.name
|
||||
end
|
||||
|
||||
test "displays child sites", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, child_site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Child Site",
|
||||
parent_site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ "Child Sites"
|
||||
assert html =~ child_site.name
|
||||
end
|
||||
|
||||
test "deletes site", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> element("button", "Delete")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert html =~ "Site deleted successfully"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, site: site} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "New" do
|
||||
test "renders new site form", %{conn: conn, organization: organization} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
|
||||
|
||||
assert html =~ "New Site"
|
||||
assert html =~ "Add a new site to your organization"
|
||||
end
|
||||
|
||||
test "creates new site", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#site-form",
|
||||
site: %{
|
||||
name: "New Site",
|
||||
location: "Building B",
|
||||
description: "New location"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/sites")
|
||||
|
||||
assert html =~ "Site created successfully"
|
||||
assert html =~ "New Site"
|
||||
end
|
||||
|
||||
test "validates required fields", %{conn: conn, organization: organization} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#site-form", site: %{name: ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Edit" do
|
||||
setup %{organization: organization} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{site: site}
|
||||
end
|
||||
|
||||
test "renders edit form", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
|
||||
|
||||
assert html =~ "Edit Site"
|
||||
assert html =~ "Update site details"
|
||||
end
|
||||
|
||||
test "updates site", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
|
||||
|
||||
{:ok, _, html} =
|
||||
view
|
||||
|> form("#site-form",
|
||||
site: %{
|
||||
name: "Updated Site",
|
||||
location: "New Location"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
|
||||
|
||||
assert html =~ "Site updated successfully"
|
||||
assert html =~ "Updated Site"
|
||||
end
|
||||
|
||||
test "requires authentication", %{organization: organization, site: site} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
|
||||
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue