towerops/test/towerops/admin/audit_logger_test.exs

342 lines
11 KiB
Elixir

defmodule Towerops.Admin.AuditLoggerTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
alias Towerops.Admin.AuditLog
alias Towerops.Admin.AuditLogger
setup do
actor = user_fixture()
target = user_fixture()
%{actor: actor, target: target}
end
describe "log_event/3" do
test "creates an audit log with a conn", %{actor: actor, target: target} do
conn =
:get
|> Plug.Test.conn("/admin/users")
|> Plug.Conn.put_req_header("user-agent", "TestBrowser/1.0")
|> Plug.Conn.put_req_header("x-forwarded-for", "203.0.113.50")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_viewed", conn,
actor_id: actor.id,
target_user_id: target.id,
metadata: %{reason: "support ticket"}
)
assert log.action == "user_data_viewed"
assert log.superuser_id == actor.id
assert log.target_user_id == target.id
assert log.ip_address.address == "203.0.113.50"
assert log.user_agent == "TestBrowser/1.0"
assert log.request_path == "/admin/users"
assert log.metadata.reason == "support ticket"
end
test "creates an audit log without a conn (nil)", %{actor: actor} do
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", nil,
actor_id: actor.id,
target_user_id: actor.id,
metadata: %{export_format: "json"}
)
assert log.action == "user_data_exported"
assert log.superuser_id == actor.id
assert log.ip_address == nil
assert log.user_agent == nil
assert log.request_path == nil
end
test "creates an audit log with no opts", %{actor: _actor} do
conn = Plug.Test.conn(:get, "/test")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn)
assert log.action == "user_data_exported"
assert log.superuser_id == nil
assert log.target_user_id == nil
assert log.request_path == "/test"
end
test "returns error for invalid action" do
assert {:error, changeset} = AuditLogger.log_event("bogus_action")
assert %{action: ["is invalid"]} = errors_on(changeset)
end
test "stores data_accessed when provided", %{actor: actor, target: target} do
data = %{fields: ["email", "phone"]}
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_viewed", nil,
actor_id: actor.id,
target_user_id: target.id,
data_accessed: data
)
assert log.data_accessed.fields == ["email", "phone"]
end
end
describe "log_user_data_viewed/4" do
test "logs user data viewed event with conn metadata", %{actor: actor, target: target} do
conn =
:get
|> Plug.Test.conn("/admin/users/#{target.id}")
|> Plug.Conn.put_req_header("user-agent", "Mozilla/5.0")
|> Plug.Conn.put_req_header("x-forwarded-for", "10.0.0.1")
data_accessed = %{fields: ["email", "name"]}
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_user_data_viewed(conn, actor.id, target.id, data_accessed)
assert log.action == "user_data_viewed"
assert log.superuser_id == actor.id
assert log.target_user_id == target.id
assert log.data_accessed.fields == ["email", "name"]
assert log.ip_address.address == "10.0.0.1"
assert log.user_agent == "Mozilla/5.0"
end
end
describe "log_user_data_exported/2" do
test "logs user data export event", %{actor: actor} do
conn = Plug.Test.conn(:post, "/settings/export")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_user_data_exported(conn, actor.id)
assert log.action == "user_data_exported"
assert log.superuser_id == actor.id
assert log.target_user_id == actor.id
assert log.metadata.export_format == "json"
end
end
describe "log_user_profile_updated/3" do
test "logs profile update with changed fields", %{actor: actor} do
conn = Plug.Test.conn(:put, "/settings/profile")
fields_changed = ["email", "name"]
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_user_profile_updated(conn, actor.id, fields_changed)
assert log.action == "user_profile_updated"
assert log.superuser_id == actor.id
assert log.target_user_id == actor.id
assert log.data_accessed.fields_changed == ["email", "name"]
end
end
describe "log_device_created/4" do
test "logs device creation with metadata", %{actor: actor} do
conn = Plug.Test.conn(:post, "/devices")
device_id = Ecto.UUID.generate()
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_device_created(conn, actor.id, device_id, "Core Router")
assert log.action == "device_created"
assert log.superuser_id == actor.id
assert log.metadata.device_id == device_id
assert log.metadata.device_name == "Core Router"
end
end
describe "log_device_updated/4" do
test "logs device update with changed fields", %{actor: actor} do
conn = Plug.Test.conn(:put, "/devices/1")
device_id = Ecto.UUID.generate()
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_device_updated(conn, actor.id, device_id, ["hostname", "ip"])
assert log.action == "device_updated"
assert log.superuser_id == actor.id
assert log.metadata.device_id == device_id
assert log.data_accessed.fields_changed == ["hostname", "ip"]
end
end
describe "log_device_deleted/4" do
test "logs device deletion with metadata", %{actor: actor} do
conn = Plug.Test.conn(:delete, "/devices/1")
device_id = Ecto.UUID.generate()
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_device_deleted(conn, actor.id, device_id, "Old Switch")
assert log.action == "device_deleted"
assert log.superuser_id == actor.id
assert log.metadata.device_id == device_id
assert log.metadata.device_name == "Old Switch"
end
end
describe "log_org_data_accessed/4" do
test "logs organization data access", %{actor: actor} do
conn = Plug.Test.conn(:get, "/orgs/settings")
org_id = Ecto.UUID.generate()
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_org_data_accessed(conn, actor.id, org_id, "viewed_members")
assert log.action == "org_data_accessed"
assert log.superuser_id == actor.id
assert log.metadata.organization_id == org_id
assert log.metadata.action == "viewed_members"
end
end
describe "log_monitoring_data_queried/4" do
test "logs monitoring query event", %{actor: actor} do
conn = Plug.Test.conn(:get, "/monitoring")
device_id = Ecto.UUID.generate()
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_monitoring_data_queried(conn, actor.id, device_id, "24h")
assert log.action == "monitoring_data_queried"
assert log.superuser_id == actor.id
assert log.metadata.device_id == device_id
assert log.metadata.time_range == "24h"
end
end
describe "log_failed_access_attempt/4" do
test "logs failed access with resource and reason", %{actor: actor} do
conn =
:get
|> Plug.Test.conn("/admin/secret")
|> Plug.Conn.put_req_header("x-forwarded-for", "192.168.1.100")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_failed_access_attempt(
conn,
actor.id,
"/admin/secret",
"insufficient_permissions"
)
assert log.action == "failed_access_attempt"
assert log.superuser_id == actor.id
assert log.metadata.resource == "/admin/secret"
assert log.metadata.reason == "insufficient_permissions"
assert log.ip_address.address == "192.168.1.100"
end
end
describe "log_privilege_escalation/5" do
test "logs role change with context", %{actor: actor} do
conn = Plug.Test.conn(:post, "/admin/roles")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_privilege_escalation(
conn,
actor.id,
"member",
"admin",
"org_promotion"
)
assert log.action == "privilege_escalation"
assert log.superuser_id == actor.id
assert log.metadata.from_role == "member"
assert log.metadata.to_role == "admin"
assert log.metadata.context == "org_promotion"
end
end
describe "IP address extraction" do
test "extracts IP from x-forwarded-for header", %{actor: actor} do
conn =
:get
|> Plug.Test.conn("/test")
|> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.ip_address.address == "198.51.100.42"
end
test "extracts first IP from comma-separated x-forwarded-for", %{actor: actor} do
conn =
:get
|> Plug.Test.conn("/test")
|> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42, 10.0.0.1, 172.16.0.1")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.ip_address.address == "198.51.100.42"
end
test "falls back to remote_ip when x-forwarded-for is absent", %{actor: actor} do
conn = %{Plug.Test.conn(:get, "/test") | remote_ip: {10, 20, 30, 40}}
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.ip_address.address == "10.20.30.40"
end
end
describe "user agent extraction" do
test "extracts user-agent header from conn", %{actor: actor} do
conn =
:get
|> Plug.Test.conn("/test")
|> Plug.Conn.put_req_header("user-agent", "CustomAgent/2.0")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.user_agent == "CustomAgent/2.0"
end
test "returns nil user_agent when header is absent", %{actor: actor} do
conn = Plug.Test.conn(:get, "/test")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.user_agent == nil
end
end
describe "request path extraction" do
test "extracts request_path from conn", %{actor: actor} do
conn = Plug.Test.conn(:get, "/admin/dashboard")
assert {:ok, %AuditLog{} = log} =
AuditLogger.log_event("user_data_exported", conn,
actor_id: actor.id,
target_user_id: actor.id
)
assert log.request_path == "/admin/dashboard"
end
end
end