more tests

This commit is contained in:
Graham McIntire 2026-02-03 11:16:43 -06:00
parent 8667d6a288
commit b78a40555c
No known key found for this signature in database
2 changed files with 678 additions and 2 deletions

View file

@ -1,11 +1,472 @@
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
import Towerops.AccountsFixtures
setup do
user = user_fixture()
superuser = user_fixture(%{superuser: true})

View file

@ -0,0 +1,215 @@
defmodule Towerops.GeoIPTest do
use Towerops.DataCase, async: true
alias Towerops.GeoIP
alias Towerops.GeoIP.Block
alias Towerops.GeoIP.Location
describe "lookup/1" do
test "returns country code for valid IP with location data" do
# Insert test location
location = insert_location(%{geoname_id: 5_375_480, country_code: "US"})
# Insert test block for IP 8.8.8.8 (134744072)
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 134_744_072,
end_ip_int: 134_744_072
})
assert GeoIP.lookup("8.8.8.8") == "US"
end
test "returns nil for valid IP with no location data" do
assert GeoIP.lookup("192.168.1.1") == nil
end
test "returns nil for invalid IP address" do
assert GeoIP.lookup("invalid") == nil
end
test "returns nil for empty string" do
assert GeoIP.lookup("") == nil
end
test "returns nil for IP with too many octets" do
assert GeoIP.lookup("1.2.3.4.5") == nil
end
test "returns nil for IP with invalid octets" do
assert GeoIP.lookup("999.999.999.999") == nil
end
end
describe "lookup_full/1" do
test "returns full location data for valid IP" do
# Insert test location with full data
location =
insert_location(%{
geoname_id: 5_375_480,
country_code: "US",
country_name: "United States",
city_name: "Mountain View",
subdivision_1_name: "California",
subdivision_2_name: nil,
latitude: 37.386,
longitude: -122.0838
})
# Insert test block for IP 8.8.8.8
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 134_744_072,
end_ip_int: 134_744_072
})
result = GeoIP.lookup_full("8.8.8.8")
assert result.country_code == "US"
assert result.country_name == "United States"
assert result.city_name == "Mountain View"
assert result.subdivision_1_name == "California"
assert result.subdivision_2_name == nil
assert result.latitude == 37.386
assert result.longitude == -122.0838
end
test "returns location data with minimal fields" do
location = insert_location(%{geoname_id: 1234, country_code: "CA"})
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 100,
end_ip_int: 200
})
result = GeoIP.lookup_full("0.0.0.100")
assert result.country_code == "CA"
assert result.country_name == nil
assert result.city_name == nil
end
test "returns nil for IP not in any block" do
# Create location and block, but query different IP
location = insert_location(%{geoname_id: 5678, country_code: "US"})
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 1000,
end_ip_int: 2000
})
assert GeoIP.lookup_full("192.168.1.1") == nil
end
test "returns nil for invalid IP string" do
assert GeoIP.lookup_full("not-an-ip") == nil
end
test "returns nil for empty string" do
assert GeoIP.lookup_full("") == nil
end
test "handles IP at start of range" do
location = insert_location(%{geoname_id: 9999, country_code: "FR"})
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 167_772_160,
end_ip_int: 167_772_170
})
# 10.0.0.0 = 167772160
result = GeoIP.lookup_full("10.0.0.0")
assert result.country_code == "FR"
end
test "handles IP at end of range" do
location = insert_location(%{geoname_id: 8888, country_code: "DE"})
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 167_772_160,
end_ip_int: 167_772_170
})
# 10.0.0.10 = 167772170
result = GeoIP.lookup_full("10.0.0.10")
assert result.country_code == "DE"
end
test "handles IP in middle of range" do
location = insert_location(%{geoname_id: 7777, country_code: "GB"})
insert_block(%{
geoname_id: location.geoname_id,
start_ip_int: 167_772_160,
end_ip_int: 167_772_170
})
# 10.0.0.5 = 167772165
result = GeoIP.lookup_full("10.0.0.5")
assert result.country_code == "GB"
end
test "returns first matching block when multiple blocks overlap" do
location1 = insert_location(%{geoname_id: 1111, country_code: "US"})
location2 = insert_location(%{geoname_id: 2222, country_code: "CA"})
insert_block(%{
network: "0.0.0.100/24",
geoname_id: location1.geoname_id,
start_ip_int: 100,
end_ip_int: 200
})
insert_block(%{
network: "0.0.0.50/24",
geoname_id: location2.geoname_id,
start_ip_int: 50,
end_ip_int: 150
})
# Query returns first match due to limit: 1
result = GeoIP.lookup_full("0.0.0.150")
assert result.country_code in ["US", "CA"]
end
end
# Helper functions to insert test data
defp insert_location(attrs) do
default_attrs = %{
geoname_id: System.unique_integer([:positive]),
country_code: "US",
country_name: nil,
city_name: nil,
subdivision_1_name: nil,
subdivision_2_name: nil,
latitude: nil,
longitude: nil
}
attrs = Map.merge(default_attrs, Map.new(attrs))
%Location{}
|> Location.changeset(attrs)
|> Repo.insert!()
end
defp insert_block(attrs) do
default_attrs = %{
network: "0.0.0.0/32",
start_ip_int: 0,
end_ip_int: 0,
geoname_id: nil,
registered_country_geoname_id: nil
}
attrs = Map.merge(default_attrs, Map.new(attrs))
%Block{}
|> Block.changeset(attrs)
|> Repo.insert!()
end
end