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, 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 }) # 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 }) # 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" }) # Sleep briefly to ensure different timestamps Process.sleep(50) 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 assert export_log["actor_email"] == user.email # Verify impersonation log assert impersonate_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"]) 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