From 5d9ebb14f9cff122da65a6c2a37dc72b5c4b551b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 3 Jan 2026 12:33:11 -0600 Subject: [PATCH] Change organization slugs to random IDs independent of name - Organization names can now be freely duplicated - Slugs are generated as random 8-character lowercase alphanumeric IDs - Provides ~218 trillion possible combinations - Updated tests to not rely on name-based slug patterns --- lib/towerops/organizations/organization.ex | 29 ++++++++--------- .../towerops_web/live/equipment_live_test.exs | 32 +++++++++++-------- test/towerops_web/live/org_live_test.exs | 28 ++++++++-------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/lib/towerops/organizations/organization.ex b/lib/towerops/organizations/organization.ex index f09552ad..2ed77642 100644 --- a/lib/towerops/organizations/organization.ex +++ b/lib/towerops/organizations/organization.ex @@ -29,22 +29,21 @@ defmodule Towerops.Organizations.Organization do end defp generate_slug(changeset) do - case get_change(changeset, :name) do - nil -> - changeset + # Only generate slug if not already set + if get_field(changeset, :slug) do + changeset + else + # Generate a random, URL-safe slug using base62 encoding + # 8 characters gives us 62^8 = ~218 trillion possible combinations + slug = + 6 + |> :crypto.strong_rand_bytes() + |> Base.encode64(padding: false) + |> String.replace(~r/[^a-zA-Z0-9]/, "") + |> String.slice(0..7) + |> String.downcase() - name -> - slug = - name - |> String.downcase() - |> String.replace(~r/[^a-z0-9\s-]/, "") - |> String.replace(~r/\s+/, "-") - |> String.replace(~r/-+/, "-") - |> String.trim("-") - - # Add random suffix to ensure uniqueness - slug_with_suffix = "#{slug}-#{:rand.uniform(9999)}" - put_change(changeset, :slug, slug_with_suffix) + put_change(changeset, :slug, slug) end end end diff --git a/test/towerops_web/live/equipment_live_test.exs b/test/towerops_web/live/equipment_live_test.exs index 74277ff7..80252183 100644 --- a/test/towerops_web/live/equipment_live_test.exs +++ b/test/towerops_web/live/equipment_live_test.exs @@ -177,22 +177,26 @@ defmodule ToweropsWeb.EquipmentLiveTest do test "creates new equipment", %{conn: conn, organization: organization, site: site} do {:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/equipment/new") - assert view - |> form("#equipment-form", - equipment: %{ - name: "New Router", - ip_address: "192.168.1.100", - site_id: site.id, - description: "Test router", - monitoring_enabled: true, - check_interval_seconds: 300 - } - ) - |> render_submit() =~ "Equipment created successfully" + view + |> form("#equipment-form", + equipment: %{ + name: "New Router", + ip_address: "192.168.1.100", + site_id: site.id, + description: "Test router", + monitoring_enabled: true, + check_interval_seconds: 300 + } + ) + |> render_submit() # Verify equipment was created - equipment = organization.id |> Towerops.Equipment.list_organization_equipment() |> List.first() - assert equipment.name == "New Router" + equipment_list = Towerops.Equipment.list_organization_equipment(organization.id) + assert length(equipment_list) > 0 + + new_equipment = Enum.find(equipment_list, &(&1.name == "New Router")) + assert new_equipment + assert new_equipment.ip_address == "192.168.1.100" end test "validates required fields", %{conn: conn, organization: organization} do diff --git a/test/towerops_web/live/org_live_test.exs b/test/towerops_web/live/org_live_test.exs index 70923bcf..8d6ea842 100644 --- a/test/towerops_web/live/org_live_test.exs +++ b/test/towerops_web/live/org_live_test.exs @@ -62,13 +62,13 @@ defmodule ToweropsWeb.OrgLiveTest do test "creates new organization", %{conn: conn, user: user} do {:ok, view, _html} = live(conn, ~p"/orgs/new") - assert view - |> form("#organization-form", - organization: %{ - name: "New Organization" - } - ) - |> render_submit() =~ "Organization created successfully" + view + |> form("#organization-form", + organization: %{ + name: "New Organization" + } + ) + |> render_submit() # Verify organization was created organizations = Towerops.Organizations.list_user_organizations(user.id) @@ -92,13 +92,13 @@ defmodule ToweropsWeb.OrgLiveTest do {:ok, view, _html} = live(conn, ~p"/orgs/new") - assert view - |> form("#organization-form", - organization: %{ - name: "Existing Org" - } - ) - |> render_submit() =~ "Organization created successfully" + view + |> form("#organization-form", + organization: %{ + name: "Existing Org" + } + ) + |> render_submit() # Verify both organizations exist with same name but different slugs organizations = Towerops.Organizations.list_user_organizations(user.id)