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:
Graham McIntire 2026-03-11 14:43:17 -05:00
parent 17fa005781
commit 7613a4dde5
No known key found for this signature in database
7 changed files with 1157 additions and 18 deletions

View file

@ -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.

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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