284 lines
9.6 KiB
Elixir
284 lines
9.6 KiB
Elixir
defmodule ToweropsWeb.Api.AccountDataControllerTest do
|
|
use ToweropsWeb.ConnCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Admin
|
|
alias Towerops.Alerts
|
|
alias Towerops.Devices
|
|
alias Towerops.Organizations
|
|
|
|
setup :register_and_log_in_user
|
|
|
|
describe "GET /api/v1/account/data" do
|
|
test "requires authentication", %{conn: _conn} do
|
|
# Build a new conn without authentication
|
|
conn = build_conn()
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
assert response(conn, 302)
|
|
assert redirected_to(conn) == ~p"/users/log-in"
|
|
end
|
|
|
|
test "returns JSON data with correct content type", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
assert response(conn, 200)
|
|
assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
|
|
end
|
|
|
|
test "sets content-disposition header for download", %{conn: conn, user: user} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
[disposition] = get_resp_header(conn, "content-disposition")
|
|
assert disposition =~ "attachment"
|
|
assert disposition =~ "towerops-data-#{user.id}"
|
|
assert disposition =~ ".json"
|
|
end
|
|
|
|
test "includes profile data in response", %{conn: conn, user: user} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert data["profile"]["id"] == user.id
|
|
assert data["profile"]["email"] == user.email
|
|
assert data["profile"]["first_name"] == user.first_name
|
|
assert data["profile"]["last_name"] == user.last_name
|
|
# Default timezone is "UTC" even if user record has nil
|
|
assert data["profile"]["timezone"] in ["UTC", user.timezone]
|
|
assert data["profile"]["is_superuser"] == user.is_superuser
|
|
end
|
|
|
|
test "includes organizations data", %{conn: conn, user: user} do
|
|
{:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["organizations"]) == 1
|
|
[org_data] = data["organizations"]
|
|
assert org_data["id"] == org.id
|
|
assert org_data["name"] == "Test Org"
|
|
assert org_data["role"] == "owner"
|
|
end
|
|
|
|
test "includes devices data with redacted SNMP community", %{conn: conn, user: user} do
|
|
{:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: org.id,
|
|
snmp_enabled: true,
|
|
snmp_community: "secret-community-string"
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["devices"]) == 1
|
|
[device_data] = data["devices"]
|
|
assert device_data["id"] == device.id
|
|
assert device_data["name"] == "Test Router"
|
|
assert device_data["ip_address"] == "192.168.1.1"
|
|
# SNMP community should be redacted for security
|
|
assert device_data["snmp_community"] == "[REDACTED]"
|
|
assert device_data["organization"]["name"] == "Test Org"
|
|
assert device_data["site"]["name"] == "Test Site"
|
|
end
|
|
|
|
test "returns empty devices when user has no organizations", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert data["devices"] == []
|
|
end
|
|
|
|
test "includes alerts data from last 90 days", %{conn: conn, user: user} do
|
|
{:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: org.id
|
|
})
|
|
|
|
# Create a recent alert
|
|
{:ok, alert} =
|
|
Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: "device_down",
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device down"
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["alerts"]) == 1
|
|
[alert_data] = data["alerts"]
|
|
assert alert_data["id"] == alert.id
|
|
assert alert_data["alert_type"] == "device_down"
|
|
assert alert_data["message"] == "Device down"
|
|
assert alert_data["device"]["name"] == "Test Router"
|
|
end
|
|
|
|
test "includes all alerts regardless of age (filtered by inserted_at, not triggered_at)", %{
|
|
conn: conn,
|
|
user: user
|
|
} do
|
|
{:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: org.id
|
|
})
|
|
|
|
# Create an alert with an old triggered_at (120 days ago)
|
|
# Note: The API filters by inserted_at (when record was created), not triggered_at
|
|
{:ok, alert} =
|
|
Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: "device_down",
|
|
triggered_at: DateTime.add(DateTime.utc_now(), -120, :day),
|
|
message: "Old triggered time but recent record"
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
# Alert should be included because it was recently inserted (even though triggered long ago)
|
|
assert length(data["alerts"]) == 1
|
|
assert hd(data["alerts"])["id"] == alert.id
|
|
end
|
|
|
|
test "returns empty alerts when user has no organizations", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert data["alerts"] == []
|
|
end
|
|
|
|
test "includes audit logs data", %{conn: conn, user: user} do
|
|
# Create superuser for audit log
|
|
superuser = user_fixture(%{superuser: true})
|
|
|
|
{:ok, _log} =
|
|
Admin.create_audit_log(%{
|
|
action: "impersonate_start",
|
|
superuser_id: superuser.id,
|
|
target_user_id: user.id,
|
|
ip_address: "127.0.0.1"
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["audit_logs"]) == 2
|
|
|
|
# Find the logs by action (ordering might vary slightly)
|
|
export_log = Enum.find(data["audit_logs"], &(&1["action"] == "user_data_exported"))
|
|
impersonate_log = Enum.find(data["audit_logs"], &(&1["action"] == "impersonate_start"))
|
|
|
|
# Verify export log
|
|
assert export_log["actor_email"] == user.email
|
|
|
|
# Verify impersonation log
|
|
assert impersonate_log["target_user_id"] == user.id
|
|
assert impersonate_log["actor_email"] == superuser.email
|
|
end
|
|
|
|
test "includes export metadata", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert data["export_info"]["format"] == "JSON"
|
|
assert data["export_info"]["gdpr_article"] == "Article 15 - Right to Access"
|
|
assert is_binary(data["export_info"]["exported_at"]) and byte_size(data["export_info"]["exported_at"]) > 0
|
|
end
|
|
|
|
test "creates audit log entry for data export", %{conn: conn, user: user} do
|
|
# Count existing audit logs
|
|
initial_count = user.id |> Admin.list_audit_logs_for_user() |> length()
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
assert response(conn, 200)
|
|
|
|
# Should have one more audit log
|
|
logs = Admin.list_audit_logs_for_user(user.id)
|
|
assert length(logs) == initial_count + 1
|
|
|
|
# Most recent log should be the data export
|
|
[latest_log | _] = logs
|
|
assert latest_log.action == "user_data_exported"
|
|
assert latest_log.superuser_id == user.id
|
|
end
|
|
|
|
test "handles user with multiple organizations", %{conn: conn, user: user} do
|
|
{:ok, _org1} = Organizations.create_organization(%{name: "Org 1"}, user.id)
|
|
|
|
# Create second org by adding membership instead of creating new one
|
|
# (to avoid subscription limit issues in tests)
|
|
other_user = user_fixture()
|
|
{:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, other_user.id)
|
|
|
|
{:ok, _membership} =
|
|
Organizations.create_membership(%{
|
|
organization_id: org2.id,
|
|
user_id: user.id,
|
|
role: :member
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["organizations"]) == 2
|
|
org_names = data["organizations"] |> Enum.map(& &1["name"]) |> Enum.sort()
|
|
assert org_names == ["Org 1", "Org 2"]
|
|
end
|
|
|
|
test "handles user as member (not owner) of organization", %{conn: conn, user: user} do
|
|
# Create another user who owns the org
|
|
owner = user_fixture()
|
|
{:ok, org} = Organizations.create_organization(%{name: "Owner's Org"}, owner.id)
|
|
|
|
# Add current user as member
|
|
{:ok, _membership} =
|
|
Organizations.create_membership(%{
|
|
organization_id: org.id,
|
|
user_id: user.id,
|
|
role: :member
|
|
})
|
|
|
|
conn = get(conn, ~p"/api/v1/account/data")
|
|
data = json_response(conn, 200)
|
|
|
|
assert length(data["organizations"]) == 1
|
|
[org_data] = data["organizations"]
|
|
assert org_data["name"] == "Owner's Org"
|
|
assert org_data["role"] == "member"
|
|
end
|
|
end
|
|
end
|