add missing REST API controller tests
Tests for agents, alerts, integrations, organization, members, and invitations controllers. Refactor alert filters to reduce complexity. 75 new tests, all passing.
This commit is contained in:
parent
17fa005781
commit
7613a4dde5
7 changed files with 1157 additions and 18 deletions
|
|
@ -197,26 +197,29 @@ defmodule Towerops.Alerts do
|
|||
def list_organization_alerts(organization_id, filters) when is_map(filters) do
|
||||
limit = filters["limit"] || 100
|
||||
|
||||
# Use denormalized organization_id for fast query
|
||||
query =
|
||||
from(a in Alert,
|
||||
where: a.organization_id == ^organization_id,
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [device: [:site], acknowledged_by: []]
|
||||
)
|
||||
|
||||
query =
|
||||
case filters["status"] do
|
||||
"active" -> where(query, [a], is_nil(a.resolved_at) and is_nil(a.acknowledged_at))
|
||||
"acknowledged" -> where(query, [a], is_nil(a.resolved_at) and not is_nil(a.acknowledged_at))
|
||||
"resolved" -> where(query, [a], not is_nil(a.resolved_at))
|
||||
_ -> query
|
||||
end
|
||||
|
||||
Repo.all(query)
|
||||
from(a in Alert,
|
||||
where: a.organization_id == ^organization_id,
|
||||
order_by: [desc: a.triggered_at],
|
||||
limit: ^limit,
|
||||
preload: [device: [:site], acknowledged_by: []]
|
||||
)
|
||||
|> filter_by_status(filters["status"])
|
||||
|> filter_by_device(filters["device_id"])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp filter_by_status(query, "active"), do: where(query, [a], is_nil(a.resolved_at) and is_nil(a.acknowledged_at))
|
||||
|
||||
defp filter_by_status(query, "acknowledged"),
|
||||
do: where(query, [a], is_nil(a.resolved_at) and not is_nil(a.acknowledged_at))
|
||||
|
||||
defp filter_by_status(query, "resolved"), do: where(query, [a], not is_nil(a.resolved_at))
|
||||
|
||||
defp filter_by_status(query, _), do: query
|
||||
|
||||
defp filter_by_device(query, nil), do: query
|
||||
defp filter_by_device(query, device_id), do: where(query, [a], a.device_id == ^device_id)
|
||||
|
||||
@doc """
|
||||
Returns alerts for multiple organizations (for GDPR data access).
|
||||
Accepts a `since` option to filter alerts created after a specific date.
|
||||
|
|
|
|||
187
test/towerops_web/controllers/api/v1/agents_controller_test.exs
Normal file
187
test/towerops_web/controllers/api/v1/agents_controller_test.exs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
defmodule ToweropsWeb.Api.V1.AgentsControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.AgentsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
}
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists agents for the authenticated organization", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, agent1, _raw1} = agent_token_fixture(organization.id, "Agent Alpha")
|
||||
{:ok, agent2, _raw2} = agent_token_fixture(organization.id, "Agent Beta")
|
||||
|
||||
conn = get(conn, ~p"/api/v1/agents")
|
||||
|
||||
assert %{"data" => agents} = json_response(conn, 200)
|
||||
assert length(agents) == 2
|
||||
|
||||
agent_ids = Enum.map(agents, & &1["id"])
|
||||
assert agent1.id in agent_ids
|
||||
assert agent2.id in agent_ids
|
||||
|
||||
agent_json = List.first(agents)
|
||||
assert Map.has_key?(agent_json, "id")
|
||||
assert Map.has_key?(agent_json, "name")
|
||||
assert Map.has_key?(agent_json, "enabled")
|
||||
assert Map.has_key?(agent_json, "last_seen_at")
|
||||
assert Map.has_key?(agent_json, "last_ip")
|
||||
assert Map.has_key?(agent_json, "metadata")
|
||||
assert Map.has_key?(agent_json, "inserted_at")
|
||||
end
|
||||
|
||||
test "returns empty list when organization has no agents", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/agents")
|
||||
|
||||
assert %{"data" => []} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "does not return agents from other organizations", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
{:ok, _other_agent, _raw} = agent_token_fixture(other_org.id)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/agents")
|
||||
|
||||
assert %{"data" => agents} = json_response(conn, 200)
|
||||
assert Enum.empty?(agents)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create/2" do
|
||||
test "creates agent with valid name and returns token", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
conn = post(conn, ~p"/api/v1/agents", %{"name" => "New Agent"})
|
||||
|
||||
assert %{"data" => agent} = json_response(conn, 201)
|
||||
assert agent["name"] == "New Agent"
|
||||
assert is_binary(agent["id"])
|
||||
assert is_binary(agent["token"])
|
||||
assert agent["enabled"] == true
|
||||
|
||||
# Verify the agent was created in the correct organization
|
||||
created = Towerops.Agents.get_agent_token!(agent["id"])
|
||||
assert created.organization_id == organization.id
|
||||
end
|
||||
|
||||
test "returns 400 without name parameter", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/agents", %{})
|
||||
|
||||
assert %{"error" => "Missing 'name' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns agent with device_count", %{conn: conn, organization: organization} do
|
||||
{:ok, agent, _raw} = agent_token_fixture(organization.id, "Show Agent")
|
||||
|
||||
conn = get(conn, ~p"/api/v1/agents/#{agent.id}")
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == agent.id
|
||||
assert data["name"] == "Show Agent"
|
||||
assert Map.has_key?(data, "device_count")
|
||||
assert data["device_count"] == 0
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent agent", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/agents/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Agent not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 for agent in another organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
{:ok, other_agent, _raw} = agent_token_fixture(other_org.id)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/agents/#{other_agent.id}")
|
||||
|
||||
assert %{"error" => "Agent not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "removes agent and returns 204", %{conn: conn, organization: organization} do
|
||||
{:ok, agent, _raw} = agent_token_fixture(organization.id)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/agents/#{agent.id}")
|
||||
|
||||
assert response(conn, 204)
|
||||
|
||||
# Verify the agent was deleted
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Towerops.Agents.get_agent_token!(agent.id)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent agent", %{conn: conn} do
|
||||
conn = delete(conn, ~p"/api/v1/agents/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Agent not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 for agent in another organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
{:ok, other_agent, _raw} = agent_token_fixture(other_org.id)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/agents/#{other_agent.id}")
|
||||
|
||||
assert %{"error" => "Agent not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/agents")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/agents")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
254
test/towerops_web/controllers/api/v1/alerts_controller_test.exs
Normal file
254
test/towerops_web/controllers/api/v1/alerts_controller_test.exs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
defmodule ToweropsWeb.Api.V1.AlertsControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.DevicesFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.ApiTokens
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
}
|
||||
end
|
||||
|
||||
defp create_alert(device, attrs \\ %{}) do
|
||||
default_attrs = %{
|
||||
device_id: device.id,
|
||||
alert_type: "ping_down",
|
||||
message: "Device unreachable",
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
}
|
||||
|
||||
{:ok, alert} = Alerts.create_alert(Map.merge(default_attrs, attrs))
|
||||
alert
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists alerts for the authenticated organization", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device = device_fixture(%{organization_id: organization.id})
|
||||
alert1 = create_alert(device)
|
||||
alert2 = create_alert(device, %{alert_type: "device_down", message: "SNMP timeout"})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts")
|
||||
|
||||
assert %{"data" => alerts} = json_response(conn, 200)
|
||||
assert length(alerts) == 2
|
||||
|
||||
alert_ids = Enum.map(alerts, & &1["id"])
|
||||
assert alert1.id in alert_ids
|
||||
assert alert2.id in alert_ids
|
||||
|
||||
alert_json = List.first(alerts)
|
||||
assert Map.has_key?(alert_json, "id")
|
||||
assert Map.has_key?(alert_json, "alert_type")
|
||||
assert Map.has_key?(alert_json, "message")
|
||||
assert Map.has_key?(alert_json, "triggered_at")
|
||||
assert Map.has_key?(alert_json, "acknowledged_at")
|
||||
assert Map.has_key?(alert_json, "resolved_at")
|
||||
assert Map.has_key?(alert_json, "device_id")
|
||||
assert Map.has_key?(alert_json, "device_name")
|
||||
assert Map.has_key?(alert_json, "acknowledged_by_email")
|
||||
assert Map.has_key?(alert_json, "gaiia_impact")
|
||||
assert Map.has_key?(alert_json, "inserted_at")
|
||||
end
|
||||
|
||||
test "returns empty list when organization has no alerts", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/alerts")
|
||||
|
||||
assert %{"data" => []} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "filters by status param for open alerts", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device = device_fixture(%{organization_id: organization.id})
|
||||
_open_alert = create_alert(device)
|
||||
|
||||
resolved_alert = create_alert(device, %{alert_type: "device_down", message: "Resolved one"})
|
||||
Alerts.resolve_alert(resolved_alert)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts?status=active")
|
||||
|
||||
assert %{"data" => alerts} = json_response(conn, 200)
|
||||
assert length(alerts) == 1
|
||||
|
||||
[returned] = alerts
|
||||
assert is_nil(returned["resolved_at"])
|
||||
end
|
||||
|
||||
test "filters by device_id param", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device1 = device_fixture(%{organization_id: organization.id})
|
||||
device2 = device_fixture(%{organization_id: organization.id})
|
||||
|
||||
_alert1 = create_alert(device1)
|
||||
_alert2 = create_alert(device2)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts?device_id=#{device1.id}")
|
||||
|
||||
assert %{"data" => alerts} = json_response(conn, 200)
|
||||
assert length(alerts) == 1
|
||||
assert List.first(alerts)["device_id"] == device1.id
|
||||
end
|
||||
|
||||
test "does not return alerts from other organizations", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_device = device_fixture(%{organization_id: other_org.id})
|
||||
_other_alert = create_alert(other_device)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts")
|
||||
|
||||
assert %{"data" => []} = json_response(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns alert details when it belongs to the organization", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device = device_fixture(%{organization_id: organization.id})
|
||||
alert = create_alert(device)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts/#{alert.id}")
|
||||
|
||||
assert %{"data" => alert_json} = json_response(conn, 200)
|
||||
assert alert_json["id"] == alert.id
|
||||
assert alert_json["alert_type"] == "ping_down"
|
||||
assert alert_json["message"] == "Device unreachable"
|
||||
assert alert_json["device_id"] == device.id
|
||||
assert alert_json["device_name"] == device.name
|
||||
end
|
||||
|
||||
test "returns 404 when alert does not exist", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/alerts/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when alert belongs to a different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_device = device_fixture(%{organization_id: other_org.id})
|
||||
other_alert = create_alert(other_device)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/alerts/#{other_alert.id}")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "acknowledge/2" do
|
||||
test "sets acknowledged_at on the alert", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device = device_fixture(%{organization_id: organization.id})
|
||||
alert = create_alert(device)
|
||||
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{alert.id}/acknowledge")
|
||||
|
||||
assert %{"data" => alert_json} = json_response(conn, 200)
|
||||
assert alert_json["id"] == alert.id
|
||||
assert alert_json["acknowledged_at"]
|
||||
end
|
||||
|
||||
test "returns 404 when alert belongs to a different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_device = device_fixture(%{organization_id: other_org.id})
|
||||
other_alert = create_alert(other_device)
|
||||
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{other_alert.id}/acknowledge")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when alert does not exist", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{Ecto.UUID.generate()}/acknowledge")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "resolve/2" do
|
||||
test "sets resolved_at on the alert", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
device = device_fixture(%{organization_id: organization.id})
|
||||
alert = create_alert(device)
|
||||
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{alert.id}/resolve")
|
||||
|
||||
assert %{"data" => alert_json} = json_response(conn, 200)
|
||||
assert alert_json["id"] == alert.id
|
||||
assert alert_json["resolved_at"]
|
||||
end
|
||||
|
||||
test "returns 404 when alert belongs to a different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_device = device_fixture(%{organization_id: other_org.id})
|
||||
other_alert = create_alert(other_device)
|
||||
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{other_alert.id}/resolve")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when alert does not exist", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/alerts/#{Ecto.UUID.generate()}/resolve")
|
||||
|
||||
assert %{"error" => "Alert not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/alerts")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/alerts")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
defmodule ToweropsWeb.Api.V1.IntegrationsControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.IntegrationsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
}
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists all integrations for authenticated organization", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
integration1 = integration_fixture(organization.id, %{provider: "preseem"})
|
||||
integration2 = integration_fixture(organization.id, %{provider: "gaiia"})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/integrations")
|
||||
|
||||
assert %{"data" => integrations} = json_response(conn, 200)
|
||||
assert length(integrations) == 2
|
||||
|
||||
ids = Enum.map(integrations, & &1["id"])
|
||||
assert integration1.id in ids
|
||||
assert integration2.id in ids
|
||||
|
||||
integration_json = List.first(integrations)
|
||||
assert Map.has_key?(integration_json, "id")
|
||||
assert Map.has_key?(integration_json, "provider")
|
||||
assert Map.has_key?(integration_json, "enabled")
|
||||
assert Map.has_key?(integration_json, "sync_interval_minutes")
|
||||
assert Map.has_key?(integration_json, "last_synced_at")
|
||||
assert Map.has_key?(integration_json, "last_sync_status")
|
||||
assert Map.has_key?(integration_json, "last_sync_message")
|
||||
assert Map.has_key?(integration_json, "inserted_at")
|
||||
assert Map.has_key?(integration_json, "updated_at")
|
||||
end
|
||||
|
||||
test "returns empty list when organization has no integrations", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/integrations")
|
||||
|
||||
assert %{"data" => []} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "does not return integrations from other organizations", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
_other_integration = integration_fixture(other_org.id)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/integrations")
|
||||
|
||||
assert %{"data" => integrations} = json_response(conn, 200)
|
||||
assert Enum.empty?(integrations)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create/2" do
|
||||
test "creates integration with valid params", %{conn: conn} do
|
||||
params = %{
|
||||
"integration" => %{
|
||||
"provider" => "preseem",
|
||||
"enabled" => true,
|
||||
"credentials" => %{"api_key" => "test-key-123"}
|
||||
}
|
||||
}
|
||||
|
||||
conn = post(conn, ~p"/api/v1/integrations", params)
|
||||
|
||||
assert %{"data" => integration} = json_response(conn, 201)
|
||||
assert integration["provider"] == "preseem"
|
||||
assert integration["enabled"] == true
|
||||
assert is_binary(integration["id"])
|
||||
end
|
||||
|
||||
test "returns 400 when integration parameter is missing", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/integrations", %{})
|
||||
|
||||
assert %{"error" => "Missing 'integration' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns integration when it belongs to organization", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
integration = integration_fixture(organization.id, %{provider: "preseem"})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/integrations/#{integration.id}")
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == integration.id
|
||||
assert data["provider"] == "preseem"
|
||||
assert data["enabled"] == true
|
||||
end
|
||||
|
||||
test "returns 404 when integration belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_integration = integration_fixture(other_org.id)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/integrations/#{other_integration.id}")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when integration does not exist", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/integrations/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "updates integration with valid attributes", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
integration = integration_fixture(organization.id, %{provider: "preseem", enabled: true})
|
||||
|
||||
update_params = %{
|
||||
"integration" => %{
|
||||
"enabled" => false
|
||||
}
|
||||
}
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/integrations/#{integration.id}", update_params)
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == integration.id
|
||||
assert data["enabled"] == false
|
||||
end
|
||||
|
||||
test "returns 400 when integration parameter is missing", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
integration = integration_fixture(organization.id)
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/integrations/#{integration.id}", %{})
|
||||
|
||||
assert %{"error" => "Missing 'integration' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
|
||||
test "returns 404 when integration belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_integration = integration_fixture(other_org.id)
|
||||
|
||||
update_params = %{
|
||||
"integration" => %{
|
||||
"enabled" => false
|
||||
}
|
||||
}
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/integrations/#{other_integration.id}", update_params)
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "deletes integration successfully", %{conn: conn, organization: organization} do
|
||||
integration = integration_fixture(organization.id)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/integrations/#{integration.id}")
|
||||
|
||||
assert response(conn, 204)
|
||||
end
|
||||
|
||||
test "returns 404 when integration belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_integration = integration_fixture(other_org.id)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/integrations/#{other_integration.id}")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when integration does not exist", %{conn: conn} do
|
||||
conn = delete(conn, ~p"/api/v1/integrations/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "test_connection/2" do
|
||||
test "returns ok status for valid integration", %{
|
||||
conn: conn,
|
||||
organization: organization
|
||||
} do
|
||||
integration = integration_fixture(organization.id, %{provider: "preseem"})
|
||||
|
||||
conn = post(conn, ~p"/api/v1/integrations/#{integration.id}/test")
|
||||
|
||||
assert %{"data" => %{"status" => "ok", "provider" => "preseem"}} =
|
||||
json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 404 when integration belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
other_integration = integration_fixture(other_org.id)
|
||||
|
||||
conn = post(conn, ~p"/api/v1/integrations/#{other_integration.id}/test")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 when integration does not exist", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/integrations/#{Ecto.UUID.generate()}/test")
|
||||
|
||||
assert %{"error" => "Integration not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/integrations")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/integrations")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
defmodule ToweropsWeb.Api.V1.InvitationsControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
alias Towerops.Organizations
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{conn: conn, user: user, organization: organization}
|
||||
end
|
||||
|
||||
defp create_test_invitation(organization, user, email \\ "invited@example.com") do
|
||||
{:ok, invitation} =
|
||||
Organizations.create_invitation(%{
|
||||
organization_id: organization.id,
|
||||
email: email,
|
||||
role: :technician,
|
||||
invited_by_id: user.id,
|
||||
token: Ecto.UUID.generate(),
|
||||
expires_at: DateTime.utc_now() |> DateTime.add(7 * 86_400, :second) |> DateTime.truncate(:second)
|
||||
})
|
||||
|
||||
invitation
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists pending invitations", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
user: user
|
||||
} do
|
||||
invitation = create_test_invitation(organization, user)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/invitations")
|
||||
|
||||
assert %{"data" => invitations} = json_response(conn, 200)
|
||||
assert length(invitations) == 1
|
||||
|
||||
inv = List.first(invitations)
|
||||
assert inv["id"] == invitation.id
|
||||
assert inv["email"] == "invited@example.com"
|
||||
assert inv["role"] == "technician"
|
||||
assert Map.has_key?(inv, "expires_at")
|
||||
assert Map.has_key?(inv, "invited_by_email")
|
||||
assert Map.has_key?(inv, "inserted_at")
|
||||
end
|
||||
|
||||
test "returns empty list when no invitations exist", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/invitations")
|
||||
|
||||
assert %{"data" => []} = json_response(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create/2" do
|
||||
test "creates invitation", %{conn: conn} do
|
||||
params = %{
|
||||
"email" => "newuser@example.com",
|
||||
"role" => "technician"
|
||||
}
|
||||
|
||||
conn = post(conn, ~p"/api/v1/invitations", params)
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 201)
|
||||
assert data["email"] == "newuser@example.com"
|
||||
assert data["role"] == "technician"
|
||||
assert Map.has_key?(data, "id")
|
||||
assert Map.has_key?(data, "expires_at")
|
||||
assert Map.has_key?(data, "inserted_at")
|
||||
end
|
||||
|
||||
test "returns 400 without required params", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/invitations", %{})
|
||||
|
||||
assert %{"error" => "Missing 'email' and 'role' parameters"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "removes invitation", %{conn: conn, organization: organization, user: user} do
|
||||
invitation = create_test_invitation(organization, user)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/invitations/#{invitation.id}")
|
||||
|
||||
assert response(conn, 204)
|
||||
end
|
||||
|
||||
test "returns 404 for invitation in different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
invitation = create_test_invitation(other_org, other_user, "other@example.com")
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/invitations/#{invitation.id}")
|
||||
|
||||
assert %{"error" => "Invitation not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent invitation", %{conn: conn} do
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/invitations/#{fake_id}")
|
||||
|
||||
assert %{"error" => "Invitation not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/invitations")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/invitations")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
155
test/towerops_web/controllers/api/v1/members_controller_test.exs
Normal file
155
test/towerops_web/controllers/api/v1/members_controller_test.exs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
defmodule ToweropsWeb.Api.V1.MembersControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
alias Towerops.Organizations
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{conn: conn, user: user, organization: organization}
|
||||
end
|
||||
|
||||
defp add_member(organization, role \\ :technician) do
|
||||
other_user = user_fixture()
|
||||
|
||||
{:ok, _membership} =
|
||||
Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: other_user.id,
|
||||
role: role
|
||||
})
|
||||
|
||||
other_user
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists members including owner", %{conn: conn, user: user} do
|
||||
conn = get(conn, ~p"/api/v1/members")
|
||||
|
||||
assert %{"data" => members} = json_response(conn, 200)
|
||||
assert [_ | _] = members
|
||||
|
||||
owner = Enum.find(members, &(&1["id"] == user.id))
|
||||
assert owner["email"] == user.email
|
||||
assert owner["role"] == "owner"
|
||||
assert Map.has_key?(owner, "inserted_at")
|
||||
end
|
||||
|
||||
test "lists multiple members", %{conn: conn, organization: organization} do
|
||||
other_user = add_member(organization, :technician)
|
||||
|
||||
conn = get(conn, ~p"/api/v1/members")
|
||||
|
||||
assert %{"data" => members} = json_response(conn, 200)
|
||||
assert length(members) == 2
|
||||
|
||||
member_ids = Enum.map(members, & &1["id"])
|
||||
assert other_user.id in member_ids
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "changes member role", %{conn: conn, organization: organization} do
|
||||
other_user = add_member(organization, :technician)
|
||||
|
||||
conn =
|
||||
patch(conn, ~p"/api/v1/members/#{other_user.id}", %{
|
||||
"role" => "admin"
|
||||
})
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == other_user.id
|
||||
assert data["role"] == "admin"
|
||||
end
|
||||
|
||||
test "returns 403 when changing owner role", %{conn: conn, user: user} do
|
||||
conn =
|
||||
patch(conn, ~p"/api/v1/members/#{user.id}", %{
|
||||
"role" => "technician"
|
||||
})
|
||||
|
||||
assert %{"error" => "Cannot change owner role"} = json_response(conn, 403)
|
||||
end
|
||||
|
||||
test "returns 404 for non-member", %{conn: conn} do
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
conn =
|
||||
patch(conn, ~p"/api/v1/members/#{fake_id}", %{
|
||||
"role" => "admin"
|
||||
})
|
||||
|
||||
assert %{"error" => "Member not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 400 without role param", %{conn: conn, organization: organization} do
|
||||
other_user = add_member(organization)
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/members/#{other_user.id}", %{})
|
||||
|
||||
assert %{"error" => "Missing 'role' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "removes member", %{conn: conn, organization: organization} do
|
||||
other_user = add_member(organization)
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/members/#{other_user.id}")
|
||||
|
||||
assert response(conn, 204)
|
||||
end
|
||||
|
||||
test "returns 403 when removing owner", %{conn: conn, user: user} do
|
||||
conn = delete(conn, ~p"/api/v1/members/#{user.id}")
|
||||
|
||||
assert %{"error" => "Cannot remove owner"} = json_response(conn, 403)
|
||||
end
|
||||
|
||||
test "returns 404 for non-member", %{conn: conn} do
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/members/#{fake_id}")
|
||||
|
||||
assert %{"error" => "Member not found"} = json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/members")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/members")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
defmodule ToweropsWeb.Api.V1.OrganizationControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
%{conn: conn, user: user, organization: organization}
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns organization data", %{conn: conn, organization: organization} do
|
||||
conn = get(conn, ~p"/api/v1/organization")
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == organization.id
|
||||
assert data["name"] == organization.name
|
||||
assert data["slug"] == organization.slug
|
||||
assert Map.has_key?(data, "subscription_plan")
|
||||
assert Map.has_key?(data, "use_sites")
|
||||
assert Map.has_key?(data, "snmp_version")
|
||||
assert Map.has_key?(data, "snmp_community")
|
||||
assert Map.has_key?(data, "snmp_port")
|
||||
assert Map.has_key?(data, "inserted_at")
|
||||
assert Map.has_key?(data, "updated_at")
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "updates organization name", %{conn: conn, organization: organization} do
|
||||
update_params = %{
|
||||
"organization" => %{
|
||||
"name" => "Updated Org Name"
|
||||
}
|
||||
}
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/organization", update_params)
|
||||
|
||||
assert %{"data" => data} = json_response(conn, 200)
|
||||
assert data["id"] == organization.id
|
||||
assert data["name"] == "Updated Org Name"
|
||||
end
|
||||
|
||||
test "returns 403 for non-owner", %{organization: organization} do
|
||||
other_user = user_fixture()
|
||||
|
||||
{:ok, _membership} =
|
||||
Towerops.Organizations.create_membership(%{
|
||||
organization_id: organization.id,
|
||||
user_id: other_user.id,
|
||||
role: :technician
|
||||
})
|
||||
|
||||
{:ok, {_token, other_raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: other_user.id,
|
||||
name: "Other Token"
|
||||
})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{other_raw_token}")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|
||||
update_params = %{
|
||||
"organization" => %{
|
||||
"name" => "Unauthorized Update"
|
||||
}
|
||||
}
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/organization", update_params)
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 403)
|
||||
end
|
||||
|
||||
test "returns 400 without organization param", %{conn: conn} do
|
||||
conn = patch(conn, ~p"/api/v1/organization", %{})
|
||||
|
||||
assert %{"error" => "Missing 'organization' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
|
||||
test "returns 422 with invalid params", %{conn: conn} do
|
||||
update_params = %{
|
||||
"organization" => %{
|
||||
"name" => ""
|
||||
}
|
||||
}
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/organization", update_params)
|
||||
|
||||
assert %{"errors" => errors} = json_response(conn, 422)
|
||||
assert Map.has_key?(errors, "name")
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
test "returns 401 without authorization header" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/organization")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
|
||||
test "returns 401 with invalid token" do
|
||||
conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer towerops_invalid_token")
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get(~p"/api/v1/organization")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue