towerops/test/towerops/organizations_test.exs

856 lines
29 KiB
Elixir

defmodule Towerops.OrganizationsTest do
use Towerops.DataCase
import Towerops.AccountsFixtures
alias Towerops.Devices.Device
alias Towerops.Organizations
describe "organizations" do
setup do
user = user_fixture()
%{user: user}
end
test "list_user_organizations/1 returns all organizations for a user", %{user: user} do
{:ok, org1} = Organizations.create_organization(%{name: "Org 1"}, user.id)
# Use bypass_limits to create second org for testing
{:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, user.id, bypass_limits: true)
orgs = Organizations.list_user_organizations(user.id)
assert length(orgs) == 2
org_ids = Enum.map(orgs, & &1.id)
assert org1.id in org_ids
assert org2.id in org_ids
end
test "list_user_organizations/1 returns empty list for user with no orgs" do
user = user_fixture()
assert Organizations.list_user_organizations(user.id) == []
end
test "get_organization!/1 returns the organization with given id", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert Organizations.get_organization!(organization.id).id == organization.id
end
test "get_organization_by_slug!/1 returns the organization with given slug", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert Organizations.get_organization_by_slug!(organization.slug).id == organization.id
end
test "create_organization/2 with valid data creates organization", %{user: user} do
valid_attrs = %{name: "New Organization"}
assert {:ok, organization} = Organizations.create_organization(valid_attrs, user.id)
assert organization.name == "New Organization"
assert organization.slug
end
test "create_organization/2 creates owner membership", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
membership = Organizations.get_membership(organization.id, user.id)
assert membership.role == :owner
end
test "create_organization/2 with invalid data returns error changeset" do
user = user_fixture()
assert {:error, changeset} = Organizations.create_organization(%{name: nil}, user.id)
assert "can't be blank" in errors_on(changeset).name
end
test "update_organization/2 with valid data updates the organization", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
update_attrs = %{name: "Updated Org"}
assert {:ok, updated} = Organizations.update_organization(organization, update_attrs)
assert updated.name == "Updated Org"
end
test "update_organization/2 with invalid data returns error changeset", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert {:error, changeset} = Organizations.update_organization(organization, %{name: nil})
assert "can't be blank" in errors_on(changeset).name
end
test "delete_organization/1 deletes the organization", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert {:ok, _} = Organizations.delete_organization(organization)
assert_raise Ecto.NoResultsError, fn -> Organizations.get_organization!(organization.id) end
end
test "change_organization/1 returns an organization changeset", %{user: user} do
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert %Ecto.Changeset{} = Organizations.change_organization(organization)
end
end
describe "memberships" do
setup do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
%{user: user, organization: organization}
end
test "get_membership/2 returns the membership", %{user: user, organization: organization} do
membership = Organizations.get_membership(organization.id, user.id)
assert membership.user_id == user.id
assert membership.organization_id == organization.id
end
test "get_membership/2 returns nil when membership doesn't exist", %{organization: organization} do
other_user = user_fixture()
assert Organizations.get_membership(organization.id, other_user.id) == nil
end
test "get_membership!/2 returns the membership", %{user: user, organization: organization} do
membership = Organizations.get_membership!(organization.id, user.id)
assert membership.user_id == user.id
end
test "get_membership!/2 raises when membership doesn't exist", %{organization: organization} do
other_user = user_fixture()
assert_raise Ecto.NoResultsError, fn ->
Organizations.get_membership!(organization.id, other_user.id)
end
end
test "list_organization_memberships/1 returns all memberships", %{
user: user,
organization: organization
} do
# Add another member
other_user = user_fixture()
{:ok, _membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: other_user.id,
role: :member
})
memberships = Organizations.list_organization_memberships(organization.id)
assert length(memberships) == 2
user_ids = Enum.map(memberships, & &1.user_id)
assert user.id in user_ids
assert other_user.id in user_ids
end
test "list_organization_notification_recipients/1 returns owners and admins", %{
organization: organization
} do
admin = user_fixture()
member = user_fixture()
{:ok, _admin_membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: admin.id,
role: :admin
})
{:ok, _member_membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: member.id,
role: :member
})
recipients = Organizations.list_organization_notification_recipients(organization.id)
recipient_ids = Enum.map(recipients, & &1.id)
# Owner and admin should be in recipients
assert admin.id in recipient_ids
# Member should NOT be in recipients
refute member.id in recipient_ids
end
test "create_membership/1 with valid data creates membership", %{organization: organization} do
new_user = user_fixture()
valid_attrs = %{
organization_id: organization.id,
user_id: new_user.id,
role: :member
}
assert {:ok, membership} = Organizations.create_membership(valid_attrs)
assert membership.role == :member
assert membership.user_id == new_user.id
end
test "create_membership/1 with invalid data returns error changeset" do
assert {:error, changeset} = Organizations.create_membership(%{})
assert "can't be blank" in errors_on(changeset).organization_id
end
test "update_membership/2 changes user role", %{user: user, organization: organization} do
membership = Organizations.get_membership!(organization.id, user.id)
assert {:ok, updated} = Organizations.update_membership(membership, %{role: :admin})
assert updated.role == :admin
end
test "delete_membership/1 deletes the membership", %{organization: organization} do
new_user = user_fixture()
{:ok, membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: new_user.id,
role: :member
})
assert {:ok, _} = Organizations.delete_membership(membership)
assert Organizations.get_membership(organization.id, new_user.id) == nil
end
end
describe "invitations" do
setup do
owner = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, owner.id)
%{owner: owner, organization: organization}
end
test "list_pending_invitations/1 returns pending invitations", %{
owner: owner,
organization: organization
} do
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "invited@example.com",
role: :member,
invited_by_id: owner.id,
token: "test-token-#{System.unique_integer()}",
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
invitations = Organizations.list_pending_invitations(organization.id)
assert length(invitations) == 1
assert hd(invitations).id == invitation.id
end
test "list_pending_invitations/1 excludes accepted invitations", %{
owner: owner,
organization: organization
} do
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "invited@example.com",
role: :member,
invited_by_id: owner.id,
token: "test-token-#{System.unique_integer()}",
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second),
accepted_at: DateTime.truncate(DateTime.utc_now(), :second)
})
invitations = Organizations.list_pending_invitations(organization.id)
refute Enum.any?(invitations, &(&1.id == invitation.id))
end
test "list_pending_invitations/1 excludes expired invitations", %{
owner: owner,
organization: organization
} do
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "invited@example.com",
role: :member,
invited_by_id: owner.id,
token: "test-token-#{System.unique_integer()}",
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), -1, :day), :second)
})
invitations = Organizations.list_pending_invitations(organization.id)
refute Enum.any?(invitations, &(&1.id == invitation.id))
end
test "create_invitation/1 with valid data creates invitation", %{
owner: owner,
organization: organization
} do
valid_attrs = %{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: "unique-token-#{System.unique_integer()}",
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
}
assert {:ok, invitation} = Organizations.create_invitation(valid_attrs)
assert invitation.email == "test@example.com"
assert invitation.role == :member
end
test "get_invitation_by_token/1 returns invitation", %{
owner: owner,
organization: organization
} do
token = "test-token-#{System.unique_integer()}"
{:ok, _invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: token,
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
invitation = Organizations.get_invitation_by_token(token)
assert invitation.token == token
end
test "get_invitation_by_token/1 returns nil for accepted invitation", %{
owner: owner,
organization: organization
} do
token = "test-token-#{System.unique_integer()}"
{:ok, _invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: token,
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second),
accepted_at: DateTime.truncate(DateTime.utc_now(), :second)
})
assert Organizations.get_invitation_by_token(token) == nil
end
test "accept_invitation/2 creates membership and marks invitation accepted", %{
owner: owner,
organization: organization
} do
token = "test-token-#{System.unique_integer()}"
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: token,
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
new_user = user_fixture()
assert {:ok, membership} = Organizations.accept_invitation(invitation, new_user.id)
assert membership.user_id == new_user.id
assert membership.role == :member
# Invitation should no longer be retrievable
assert Organizations.get_invitation_by_token(token) == nil
end
test "delete_invitation/1 deletes the invitation", %{
owner: owner,
organization: organization
} do
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: "test-token-#{System.unique_integer()}",
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
assert {:ok, _} = Organizations.delete_invitation(invitation)
assert Organizations.list_pending_invitations(organization.id) == []
end
end
describe "user_has_access?/2" do
test "returns true when user is member of organization" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
assert Organizations.user_has_access?(user.id, organization.id)
end
test "returns false when user is not member of organization" do
user1 = user_fixture()
user2 = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user1.id)
refute Organizations.user_has_access?(user2.id, organization.id)
end
end
describe "authorization" do
setup do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
membership = Organizations.get_membership!(organization.id, user.id)
%{membership: membership}
end
test "can?/3 delegates to Policy module", %{membership: membership} do
# Owner can delete organization
assert Organizations.can?(membership, :delete, :organization)
# Change to member role
{:ok, member_membership} = Organizations.update_membership(membership, %{role: :member})
# Member cannot delete organization
refute Organizations.can?(member_membership, :delete, :organization)
end
end
describe "apply_snmp_config_to_all_equipment/1" do
test "updates SNMP config for all devices in organization" do
user = user_fixture()
{:ok, organization} =
Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, organization} =
Organizations.update_organization(organization, %{
snmp_version: "2c",
snmp_community: "new-community"
})
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device1} =
Towerops.Devices.create_device(%{
name: "Device 1",
ip_address: "192.168.1.1",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
site_id: site.id,
organization_id: organization.id
})
{:ok, device2} =
Towerops.Devices.create_device(%{
name: "Device 2",
ip_address: "192.168.1.2",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
site_id: site.id,
organization_id: organization.id
})
{count, _} = Organizations.apply_snmp_config_to_all_equipment(organization.id)
assert count == 2
updated_device1 = Repo.get!(Device, device1.id)
assert updated_device1.snmp_version == "2c"
assert updated_device1.snmp_community == "new-community"
updated_device2 = Repo.get!(Device, device2.id)
assert updated_device2.snmp_version == "2c"
assert updated_device2.snmp_community == "new-community"
end
test "returns 0 when organization has no devices" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{count, _} = Organizations.apply_snmp_config_to_all_equipment(organization.id)
assert count == 0
end
end
describe "apply_agent_to_all_equipment/1" do
test "creates agent assignments for all devices when default agent is set" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, agent_token, _token} =
Towerops.Agents.create_agent_token(organization.id, "Test Agent")
{:ok, organization} =
Organizations.update_organization(organization, %{
default_agent_token_id: agent_token.id
})
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device1} =
Towerops.Devices.create_device(%{
name: "Device 1",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id
})
{:ok, device2} =
Towerops.Devices.create_device(%{
name: "Device 2",
ip_address: "192.168.1.2",
site_id: site.id,
organization_id: organization.id
})
{count, _} = Organizations.apply_agent_to_all_equipment(organization.id)
assert count == 2
assignment1 = Towerops.Agents.get_device_assignment(device1.id)
assert assignment1.agent_token_id == agent_token.id
assignment2 = Towerops.Agents.get_device_assignment(device2.id)
assert assignment2.agent_token_id == agent_token.id
end
test "deletes agent assignments when default agent is nil" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Test Device",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id
})
{:ok, agent_token, _token} =
Towerops.Agents.create_agent_token(organization.id, "Test Agent")
Towerops.Agents.assign_device_to_agent(agent_token.id, device.id)
# Organization has no default agent set (nil)
Organizations.apply_agent_to_all_equipment(organization.id)
assert Towerops.Agents.get_device_assignment(device.id) == nil
end
test "replaces existing agent assignments" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, old_agent, _token1} =
Towerops.Agents.create_agent_token(organization.id, "Old Agent")
{:ok, new_agent, _token2} =
Towerops.Agents.create_agent_token(organization.id, "New Agent")
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Test Device",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id
})
Towerops.Agents.assign_device_to_agent(old_agent.id, device.id)
{:ok, organization} =
Organizations.update_organization(organization, %{
default_agent_token_id: new_agent.id
})
Organizations.apply_agent_to_all_equipment(organization.id)
assignment = Towerops.Agents.get_device_assignment(device.id)
assert assignment.agent_token_id == new_agent.id
end
test "returns 0 when organization has no devices" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, agent_token, _token} =
Towerops.Agents.create_agent_token(organization.id, "Test Agent")
{:ok, organization} =
Organizations.update_organization(organization, %{
default_agent_token_id: agent_token.id
})
{count, _} = Organizations.apply_agent_to_all_equipment(organization.id)
assert count == 0
end
end
describe "organization creation with subscription limits" do
test "blocks creating second free organization" do
user = user_fixture()
{:ok, _org1} = Organizations.create_organization(%{name: "Free Org 1"}, user.id)
assert {:error, changeset} = Organizations.create_organization(%{name: "Free Org 2"}, user.id)
assert %{
base: [
"You already have a free organization. Upgrade your existing organization to create additional ones."
]
} =
errors_on(changeset)
end
test "allows creating first free organization" do
user = user_fixture()
assert {:ok, org} = Organizations.create_organization(%{name: "Free Org"}, user.id)
assert org.subscription_plan == "free"
end
test "bypasses limit with bypass_limits option" do
user = user_fixture()
{:ok, _org1} = Organizations.create_organization(%{name: "Free Org 1"}, user.id)
# Second free org succeeds with bypass
assert {:ok, _org2} =
Organizations.create_organization(
%{name: "Free Org 2"},
user.id,
bypass_limits: true
)
end
test "invited members can create their own free organization" do
owner = user_fixture()
member = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Owner's Org"}, owner.id)
# Add member to organization
{:ok, _membership} =
Organizations.create_membership(%{
user_id: member.id,
organization_id: organization.id,
role: :member
})
# Member can still create their own free org
assert {:ok, _org} = Organizations.create_organization(%{name: "Member's Org"}, member.id)
end
test "create_organization/3 with explicit free subscription plan" do
user = user_fixture()
# Explicitly specify "free" subscription plan
assert {:ok, org} =
Organizations.create_organization(
%{name: "Free Org", subscription_plan: "free"},
user.id
)
assert org.subscription_plan == "free"
end
end
describe "update_organization/2 MikroTik propagation" do
test "propagates MikroTik settings when changed" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "MikroTik Device",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id,
manufacturer: "MikroTik"
})
# Update organization MikroTik settings
{:ok, updated_org} =
Organizations.update_organization(organization, %{
mikrotik_username: "admin",
mikrotik_password: "password123",
mikrotik_port: 8729,
mikrotik_use_ssl: true,
mikrotik_enabled: true
})
assert updated_org.mikrotik_username == "admin"
assert updated_org.mikrotik_port == 8729
assert updated_org.mikrotik_use_ssl == true
assert updated_org.mikrotik_enabled == true
# Verify device credential source is "organization" (inherits directly from org)
updated_device = Repo.get!(Device, device.id)
assert updated_device.mikrotik_credential_source == "organization"
# Verify the device received the organization's MikroTik settings through propagation
assert updated_device.mikrotik_username == "admin"
assert updated_device.mikrotik_port == 8729
assert updated_device.mikrotik_use_ssl == true
assert updated_device.mikrotik_enabled == true
end
test "propagates when individual MikroTik fields change" do
user = user_fixture()
{:ok, organization} =
Organizations.create_organization(
%{
name: "Test Org",
mikrotik_username: "original",
mikrotik_password: "oldpass"
},
user.id
)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, _device} =
Towerops.Devices.create_device(%{
name: "MikroTik Device",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id,
manufacturer: "MikroTik"
})
# Change only username - should still propagate
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_username: "newuser"
})
# Change only password - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_password: "newpass"
})
# Change only port - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_port: 8728
})
# Change only use_ssl - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_use_ssl: false
})
# Change only enabled - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_enabled: false
})
end
end
describe "get_default_membership/1" do
test "returns the default membership for a user" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Default Org"}, user.id)
membership = Organizations.get_default_membership(user.id)
assert membership.organization_id == organization.id
assert membership.is_default == true
assert membership.organization.id == organization.id
end
test "returns nil when user has no default membership" do
user = user_fixture()
assert Organizations.get_default_membership(user.id) == nil
end
end
describe "set_default_organization/2" do
test "sets an organization as the user's default" do
user = user_fixture()
{:ok, _org1} = Organizations.create_organization(%{name: "Org 1"}, user.id)
{:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, user.id, bypass_limits: true)
assert {:ok, returned_org} = Organizations.set_default_organization(user.id, org2.id)
assert returned_org.id == org2.id
# Verify org2 is now default
membership = Organizations.get_default_membership(user.id)
assert membership.organization_id == org2.id
end
test "returns error when user has no access to organization" do
user1 = user_fixture()
user2 = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user1.id)
assert {:error, :not_found} = Organizations.set_default_organization(user2.id, organization.id)
end
end
describe "accept_invitation/2 error handling" do
test "returns error when membership creation fails" do
owner = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, owner.id)
token = "test-token-#{System.unique_integer()}"
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: token,
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
# Create a membership first so the accept will fail with unique constraint
new_user = user_fixture()
{:ok, _existing_membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: new_user.id,
role: :admin
})
# Accepting invitation should fail because membership already exists
assert {:error, _changeset} = Organizations.accept_invitation(invitation, new_user.id)
end
end
end