towerops/test/towerops/admin_test.exs
2026-02-03 11:16:43 -06:00

655 lines
20 KiB
Elixir

defmodule Towerops.AdminTest do
use Towerops.DataCase
import Towerops.AccountsFixtures
alias Towerops.Accounts.User
alias Towerops.Admin
alias Towerops.Organizations.Organization
describe "create_audit_log/1" do
test "creates audit log with valid attributes" do
superuser = user_fixture(%{superuser: true})
target_user = user_fixture()
attrs = %{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "192.168.1.1"
}
assert {:ok, audit_log} = Admin.create_audit_log(attrs)
assert audit_log.action == "impersonate_start"
assert audit_log.superuser_id == superuser.id
assert audit_log.target_user_id == target_user.id
assert audit_log.ip_address.address == "192.168.1.1"
end
test "creates audit log with nil target_user_id" do
superuser = user_fixture(%{superuser: true})
attrs = %{
action: "user_data_exported",
superuser_id: superuser.id,
target_user_id: nil,
ip_address: "10.0.0.1"
}
assert {:ok, audit_log} = Admin.create_audit_log(attrs)
assert audit_log.target_user_id == nil
end
test "creates audit log with only required field (action)" do
attrs = %{
action: "impersonate_start"
# Only action is required, superuser_id and ip_address are optional
}
assert {:ok, audit_log} = Admin.create_audit_log(attrs)
assert audit_log.action == "impersonate_start"
assert audit_log.superuser_id == nil
assert audit_log.ip_address == nil
end
test "returns error with invalid action" do
superuser = user_fixture(%{superuser: true})
attrs = %{
action: "invalid_action",
superuser_id: superuser.id,
ip_address: "127.0.0.1"
}
assert {:error, changeset} = Admin.create_audit_log(attrs)
assert %{action: ["is invalid"]} = errors_on(changeset)
end
test "returns error with missing action" do
superuser = user_fixture(%{superuser: true})
attrs = %{
superuser_id: superuser.id,
ip_address: "127.0.0.1"
}
assert {:error, changeset} = Admin.create_audit_log(attrs)
assert %{action: ["can't be blank"]} = errors_on(changeset)
end
end
describe "list_audit_logs/1" do
setup do
superuser = user_fixture(%{superuser: true})
target_user = user_fixture()
%{superuser: superuser, target_user: target_user}
end
test "returns all audit logs with default pagination", %{superuser: superuser, target_user: target_user} do
{:ok, _log1} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
{:ok, _log2} =
Admin.create_audit_log(%{
action: "impersonate_end",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs()
assert length(result) == 2
end
test "respects limit parameter", %{superuser: superuser, target_user: target_user} do
for _i <- 1..5 do
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
end
result = Admin.list_audit_logs(limit: 3)
assert length(result) == 3
end
test "orders logs by inserted_at descending", %{superuser: superuser, target_user: target_user} do
{:ok, _log1} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
{:ok, _log2} =
Admin.create_audit_log(%{
action: "impersonate_end",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs()
assert length(result) == 2
# Verify ordering: each record's inserted_at should be >= next record
[first, second] = result
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
end
test "preloads superuser and target_user associations", %{superuser: superuser, target_user: target_user} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs()
assert length(result) == 1
log = hd(result)
assert Ecto.assoc_loaded?(log.superuser)
assert Ecto.assoc_loaded?(log.target_user)
end
test "returns empty list when no audit logs exist" do
result = Admin.list_audit_logs()
assert result == []
end
end
describe "list_all_users/1" do
test "returns all users with default pagination" do
user1 = user_fixture()
user2 = user_fixture()
user3 = user_fixture()
result = Admin.list_all_users()
assert length(result) >= 3
user_ids = Enum.map(result, & &1.id)
assert user1.id in user_ids
assert user2.id in user_ids
assert user3.id in user_ids
end
test "respects limit parameter" do
for _i <- 1..10 do
user_fixture()
end
result = Admin.list_all_users(limit: 5)
assert length(result) == 5
end
test "respects offset parameter" do
# Create users in a clean test
Repo.delete_all(User)
_users = for _i <- 1..10, do: user_fixture()
result = Admin.list_all_users(offset: 7)
assert length(result) == 3
end
test "combines limit and offset" do
Repo.delete_all(User)
for _i <- 1..20 do
user_fixture()
end
result = Admin.list_all_users(limit: 5, offset: 10)
assert length(result) == 5
end
test "orders users by inserted_at descending" do
Repo.delete_all(User)
_user1 = user_fixture()
_user2 = user_fixture()
_user3 = user_fixture()
result = Admin.list_all_users()
assert length(result) == 3
# Verify descending order: each user's inserted_at should be >= next user
[first, second, third] = result
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
assert DateTime.compare(second.inserted_at, third.inserted_at) in [:gt, :eq]
end
test "returns empty list when no users exist" do
# Clean up any existing users
Repo.delete_all(User)
result = Admin.list_all_users()
assert result == []
end
test "preloads organizations association" do
user = user_fixture()
result = Admin.list_all_users()
found_user = Enum.find(result, &(&1.id == user.id))
assert Ecto.assoc_loaded?(found_user.organizations)
end
end
describe "count_users/0" do
test "returns count of all users" do
Repo.delete_all(User)
assert Admin.count_users() == 0
user_fixture()
assert Admin.count_users() == 1
user_fixture()
user_fixture()
assert Admin.count_users() == 3
end
end
describe "delete_user/3" do
test "deletes user and creates audit log" do
superuser = user_fixture(%{superuser: true})
target_user = user_fixture()
target_user_id = target_user.id
target_user_email = target_user.email
assert {:ok, _deleted_user} = Admin.delete_user(target_user_id, superuser.id, "192.168.1.1")
# User should be deleted
refute Repo.get(User, target_user_id)
# Audit log should be created
audit_logs = Admin.list_audit_logs()
assert length(audit_logs) == 1
log = hd(audit_logs)
assert log.action == "user_delete"
assert log.superuser_id == superuser.id
# target_user_id may be nil due to foreign key constraints after deletion
assert log.ip_address.address == "192.168.1.1"
assert log.metadata["email"] == target_user_email
end
test "raises exception when user does not exist" do
superuser = user_fixture(%{superuser: true})
non_existent_id = Ecto.UUID.generate()
assert_raise Ecto.NoResultsError, fn ->
Admin.delete_user(non_existent_id, superuser.id, "127.0.0.1")
end
end
test "anonymizes login history before deletion" do
superuser = user_fixture(%{superuser: true})
target_user = user_fixture()
# Create a login attempt for the target user
{:ok, _login} =
Towerops.Accounts.record_login_attempt(%{
user_id: target_user.id,
email: target_user.email,
success: true,
method: "password",
ip_address: "10.0.0.1"
})
assert {:ok, _deleted_user} = Admin.delete_user(target_user.id, superuser.id, "192.168.1.1")
# User should be deleted
refute Repo.get(User, target_user.id)
# Login attempt should be anonymized (user_id set to nil, anonymized_at set)
login_attempt = Repo.get_by(Towerops.Accounts.LoginAttempt, email: target_user.email)
if login_attempt do
assert is_nil(login_attempt.user_id)
assert login_attempt.anonymized_at
end
end
end
describe "list_all_organizations/1" do
test "returns all organizations with default pagination" do
# Each user can only create one free organization, so use different users
user1 = user_fixture()
user2 = user_fixture()
user3 = user_fixture()
{:ok, org1} = Towerops.Organizations.create_organization(%{name: "Org 1"}, user1.id)
{:ok, org2} = Towerops.Organizations.create_organization(%{name: "Org 2"}, user2.id)
{:ok, org3} = Towerops.Organizations.create_organization(%{name: "Org 3"}, user3.id)
result = Admin.list_all_organizations()
assert length(result) >= 3
org_ids = Enum.map(result, & &1.id)
assert org1.id in org_ids
assert org2.id in org_ids
assert org3.id in org_ids
end
test "respects limit parameter" do
for i <- 1..10 do
user = user_fixture()
Towerops.Organizations.create_organization(%{name: "Org #{i}"}, user.id)
end
result = Admin.list_all_organizations(limit: 5)
assert length(result) == 5
end
test "respects offset parameter" do
Repo.delete_all(Organization)
_orgs =
for i <- 1..10 do
user = user_fixture()
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Org #{i}"}, user.id)
org
end
result = Admin.list_all_organizations(offset: 7)
assert length(result) == 3
end
test "combines limit and offset" do
Repo.delete_all(Organization)
for i <- 1..20 do
user = user_fixture()
Towerops.Organizations.create_organization(%{name: "Org #{i}"}, user.id)
end
result = Admin.list_all_organizations(limit: 5, offset: 10)
assert length(result) == 5
end
test "orders organizations by inserted_at descending" do
Repo.delete_all(Organization)
user1 = user_fixture()
user2 = user_fixture()
user3 = user_fixture()
{:ok, _org1} = Towerops.Organizations.create_organization(%{name: "Org 1"}, user1.id)
{:ok, _org2} = Towerops.Organizations.create_organization(%{name: "Org 2"}, user2.id)
{:ok, _org3} = Towerops.Organizations.create_organization(%{name: "Org 3"}, user3.id)
result = Admin.list_all_organizations()
assert length(result) == 3
# Verify descending order: each org's inserted_at should be >= next org
[first, second, third] = result
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
assert DateTime.compare(second.inserted_at, third.inserted_at) in [:gt, :eq]
end
test "returns empty list when no organizations exist" do
Repo.delete_all(Organization)
result = Admin.list_all_organizations()
assert result == []
end
test "preloads memberships with users" do
user = user_fixture()
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
result = Admin.list_all_organizations()
found_org = Enum.find(result, &(&1.id == org.id))
assert Ecto.assoc_loaded?(found_org.memberships)
assert found_org.memberships != []
assert Ecto.assoc_loaded?(hd(found_org.memberships).user)
end
end
describe "count_organizations/0" do
test "returns count of all organizations" do
Repo.delete_all(Organization)
assert Admin.count_organizations() == 0
user1 = user_fixture()
Towerops.Organizations.create_organization(%{name: "Org 1"}, user1.id)
assert Admin.count_organizations() == 1
user2 = user_fixture()
user3 = user_fixture()
Towerops.Organizations.create_organization(%{name: "Org 2"}, user2.id)
Towerops.Organizations.create_organization(%{name: "Org 3"}, user3.id)
assert Admin.count_organizations() == 3
end
end
describe "delete_organization/3" do
test "deletes organization and creates audit log" do
superuser = user_fixture(%{superuser: true})
user = user_fixture()
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
assert {:ok, _deleted_org} = Admin.delete_organization(org.id, superuser.id, "192.168.1.1")
# Organization should be deleted
refute Repo.get(Organization, org.id)
# Audit log should be created
audit_logs = Admin.list_audit_logs()
assert length(audit_logs) == 1
log = hd(audit_logs)
assert log.action == "org_delete"
assert log.superuser_id == superuser.id
assert log.ip_address.address == "192.168.1.1"
assert log.metadata["name"] == "Test Org"
assert log.metadata["slug"] == org.slug
end
test "raises exception when organization does not exist" do
superuser = user_fixture(%{superuser: true})
non_existent_id = Ecto.UUID.generate()
assert_raise Ecto.NoResultsError, fn ->
Admin.delete_organization(non_existent_id, superuser.id, "127.0.0.1")
end
end
end
describe "GDPR data access - list_audit_logs_for_user/2" do
setup do
user = user_fixture()
superuser = user_fixture(%{superuser: true})
target_user = user_fixture()
%{user: user, superuser: superuser, target_user: target_user}
end
test "returns audit logs where user is the target", %{superuser: superuser, target_user: target_user} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(target_user.id)
assert length(result) == 1
assert hd(result).target_user_id == target_user.id
end
test "returns audit logs where user is the superuser", %{superuser: superuser, target_user: target_user} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(superuser.id)
assert length(result) == 1
assert hd(result).superuser_id == superuser.id
end
test "returns audit logs for both superuser and target actions", %{
superuser: superuser,
target_user: target_user,
user: user
} do
# User is target in one action
{:ok, _log1} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: user.id,
ip_address: "127.0.0.1"
})
# User is superuser in another action
{:ok, _log2} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: user.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(user.id)
assert length(result) == 2
end
test "does not return audit logs for other users", %{
superuser: superuser,
target_user: target_user,
user: user
} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
# User is not involved in this log
result = Admin.list_audit_logs_for_user(user.id)
assert result == []
end
test "respects limit parameter", %{superuser: superuser, target_user: target_user} do
# Create 5 audit logs
for _i <- 1..5 do
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
end
result = Admin.list_audit_logs_for_user(target_user.id, limit: 3)
assert length(result) == 3
end
test "defaults to limit 100", %{superuser: superuser, target_user: target_user} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
# Should not error with default limit
result = Admin.list_audit_logs_for_user(target_user.id)
assert length(result) == 1
end
test "orders logs by inserted_at descending", %{superuser: superuser, target_user: target_user} do
{:ok, _old_log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
# Sleep to ensure different timestamp (database uses second precision)
Process.sleep(1100)
{:ok, _new_log} =
Admin.create_audit_log(%{
action: "impersonate_end",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(target_user.id)
assert length(result) == 2
# Most recent should be first
[first, second] = result
# Verify ordering by timestamp
assert DateTime.after?(first.inserted_at, second.inserted_at)
# The newer log (impersonate_end) should come first
assert first.action == "impersonate_end"
assert second.action == "impersonate_start"
end
test "preloads superuser and target_user associations", %{
superuser: superuser,
target_user: target_user
} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "impersonate_start",
superuser_id: superuser.id,
target_user_id: target_user.id,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(target_user.id)
assert length(result) == 1
log = hd(result)
assert Ecto.assoc_loaded?(log.superuser)
assert Ecto.assoc_loaded?(log.target_user)
assert log.superuser.id == superuser.id
assert log.target_user.id == target_user.id
end
test "returns empty list when user has no audit logs", %{user: user} do
result = Admin.list_audit_logs_for_user(user.id)
assert result == []
end
test "handles audit logs with nil target_user_id", %{superuser: superuser} do
{:ok, _log} =
Admin.create_audit_log(%{
action: "user_data_exported",
superuser_id: superuser.id,
target_user_id: nil,
ip_address: "127.0.0.1"
})
result = Admin.list_audit_logs_for_user(superuser.id)
assert length(result) == 1
assert hd(result).target_user_id == nil
end
end
end