towerops/test/towerops_web/controllers/api/account_data_controller_test.exs
2026-06-15 11:15:46 -05:00

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