add preseem access points schema and migration

This commit is contained in:
Graham McIntire 2026-02-12 17:11:36 -06:00
parent 9f3eb55476
commit 5f17d5aed1
No known key found for this signature in database
3 changed files with 182 additions and 0 deletions

View file

@ -0,0 +1,65 @@
defmodule Towerops.Preseem.AccessPoint do
@moduledoc """
Schema for access points synced from Preseem API.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_match_confidences ~w(auto_mac auto_ip auto_hostname manual ambiguous unmatched)
schema "preseem_access_points" do
field :preseem_id, :string
field :name, :string
field :mac_address, :string
field :ip_address, :string
field :model, :string
field :firmware, :string
field :capacity_score, :float
field :qoe_score, :float
field :rf_score, :float
field :busy_hours, :integer
field :airtime_utilization, :float
field :subscriber_count, :integer
field :match_confidence, :string, default: "unmatched"
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
belongs_to :device, Towerops.Devices.Device
timestamps(type: :utc_datetime)
end
def changeset(access_point, attrs) do
access_point
|> cast(attrs, [
:organization_id,
:preseem_id,
:name,
:mac_address,
:ip_address,
:model,
:firmware,
:capacity_score,
:qoe_score,
:rf_score,
:busy_hours,
:airtime_utilization,
:subscriber_count,
:device_id,
:match_confidence,
:raw_data
])
|> validate_required([:organization_id, :preseem_id])
|> validate_inclusion(:match_confidence, @valid_match_confidences)
|> unique_constraint([:organization_id, :preseem_id],
error_key: :preseem_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
|> foreign_key_constraint(:device_id)
end
end

View file

@ -0,0 +1,35 @@
defmodule Towerops.Repo.Migrations.CreatePreseemAccessPoints do
use Ecto.Migration
def change do
create table(:preseem_access_points, primary_key: false) do
add :id, :binary_id, primary_key: true
add :organization_id, references(:organizations, type: :binary_id, on_delete: :delete_all),
null: false
add :preseem_id, :string, null: false
add :name, :string
add :mac_address, :string
add :ip_address, :string
add :model, :string
add :firmware, :string
add :capacity_score, :float
add :qoe_score, :float
add :rf_score, :float
add :busy_hours, :integer
add :airtime_utilization, :float
add :subscriber_count, :integer
add :device_id, references(:devices, type: :binary_id, on_delete: :nilify_all)
add :match_confidence, :string, default: "unmatched"
add :raw_data, :map
timestamps(type: :utc_datetime)
end
create unique_index(:preseem_access_points, [:organization_id, :preseem_id])
create index(:preseem_access_points, [:organization_id])
create index(:preseem_access_points, [:device_id])
create index(:preseem_access_points, [:match_confidence])
end
end

View file

@ -0,0 +1,82 @@
defmodule Towerops.Preseem.AccessPointTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Preseem.AccessPoint
setup do
user = user_fixture()
org = organization_fixture(user.id)
%{organization: org}
end
describe "changeset/2" do
test "valid changeset with required fields", %{organization: org} do
attrs = %{organization_id: org.id, preseem_id: "ap-123", name: "Tower1-AP1"}
changeset = AccessPoint.changeset(%AccessPoint{}, attrs)
assert changeset.valid?
end
test "requires organization_id" do
changeset = AccessPoint.changeset(%AccessPoint{}, %{preseem_id: "ap-1"})
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).organization_id
end
test "requires preseem_id" do
changeset = AccessPoint.changeset(%AccessPoint{}, %{organization_id: Ecto.UUID.generate()})
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).preseem_id
end
test "validates match_confidence is a known value" do
attrs = %{organization_id: Ecto.UUID.generate(), preseem_id: "ap-1", match_confidence: "bogus"}
changeset = AccessPoint.changeset(%AccessPoint{}, attrs)
refute changeset.valid?
assert "is invalid" in errors_on(changeset).match_confidence
end
test "accepts all valid match_confidence values" do
for confidence <- ~w(auto_mac auto_ip auto_hostname manual ambiguous unmatched) do
attrs = %{organization_id: Ecto.UUID.generate(), preseem_id: "ap-1", match_confidence: confidence}
changeset = AccessPoint.changeset(%AccessPoint{}, attrs)
assert changeset.valid?, "Expected #{confidence} to be valid"
end
end
test "enforces unique preseem_id per org", %{organization: org} do
attrs = %{organization_id: org.id, preseem_id: "ap-123"}
{:ok, _} = %AccessPoint{} |> AccessPoint.changeset(attrs) |> Repo.insert()
assert {:error, changeset} = %AccessPoint{} |> AccessPoint.changeset(attrs) |> Repo.insert()
assert "has already been taken" in errors_on(changeset).preseem_id
end
test "stores and retrieves raw_data", %{organization: org} do
raw = %{"extra_field" => "value", "nested" => %{"key" => 42}}
attrs = %{organization_id: org.id, preseem_id: "ap-raw", raw_data: raw}
{:ok, ap} = %AccessPoint{} |> AccessPoint.changeset(attrs) |> Repo.insert()
reloaded = Repo.get!(AccessPoint, ap.id)
assert reloaded.raw_data == raw
end
test "stores numeric scores", %{organization: org} do
attrs = %{
organization_id: org.id,
preseem_id: "ap-scores",
qoe_score: 85.5,
capacity_score: 72.0,
rf_score: 90.1,
busy_hours: 4,
airtime_utilization: 65.3,
subscriber_count: 42
}
{:ok, ap} = %AccessPoint{} |> AccessPoint.changeset(attrs) |> Repo.insert()
reloaded = Repo.get!(AccessPoint, ap.id)
assert reloaded.qoe_score == 85.5
assert reloaded.subscriber_count == 42
end
end
end