From f6509cac084ea552607de37d6d17fd7037ec53f6 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 3 Feb 2026 15:27:05 -0600 Subject: [PATCH] more tests --- .../towerops/agents/agent_assignment_test.exs | 163 +++++++++++++ test/towerops/agents/agent_token_test.exs | 228 ++++++++++++++++++ test/towerops/alerts/alert_test.exs | 178 ++++++++++++++ 3 files changed, 569 insertions(+) create mode 100644 test/towerops/agents/agent_assignment_test.exs create mode 100644 test/towerops/agents/agent_token_test.exs create mode 100644 test/towerops/alerts/alert_test.exs diff --git a/test/towerops/agents/agent_assignment_test.exs b/test/towerops/agents/agent_assignment_test.exs new file mode 100644 index 00000000..fc7d6b66 --- /dev/null +++ b/test/towerops/agents/agent_assignment_test.exs @@ -0,0 +1,163 @@ +defmodule Towerops.Agents.AgentAssignmentTest do + use Towerops.DataCase, async: true + + alias Towerops.Agents.AgentAssignment + + describe "changeset/2" do + test "valid changeset with required fields" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert changeset.changes.agent_token_id == agent_token_id + assert changeset.changes.device_id == device_id + end + + test "valid changeset with all fields" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id, + enabled: false + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert changeset.changes.agent_token_id == agent_token_id + assert changeset.changes.device_id == device_id + assert changeset.changes.enabled == false + end + + test "enabled defaults to true" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert Ecto.Changeset.get_field(changeset, :enabled) == true + end + + test "requires agent_token_id" do + device_id = Ecto.UUID.generate() + attrs = %{device_id: device_id} + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + refute changeset.valid? + assert %{agent_token_id: ["can't be blank"]} = errors_on(changeset) + end + + test "requires device_id" do + agent_token_id = Ecto.UUID.generate() + attrs = %{agent_token_id: agent_token_id} + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + refute changeset.valid? + assert %{device_id: ["can't be blank"]} = errors_on(changeset) + end + + test "accepts enabled as true" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id, + enabled: true + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert Ecto.Changeset.get_field(changeset, :enabled) == true + end + + test "accepts enabled as false" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id, + enabled: false + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert changeset.changes.enabled == false + end + + test "allows updating enabled field" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + existing_assignment = %AgentAssignment{ + id: Ecto.UUID.generate(), + agent_token_id: agent_token_id, + device_id: device_id, + enabled: true + } + + attrs = %{enabled: false} + changeset = AgentAssignment.changeset(existing_assignment, attrs) + + assert changeset.valid? + assert changeset.changes.enabled == false + end + + test "allows changing agent_token_id" do + old_agent_token_id = Ecto.UUID.generate() + new_agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + existing_assignment = %AgentAssignment{ + id: Ecto.UUID.generate(), + agent_token_id: old_agent_token_id, + device_id: device_id, + enabled: true + } + + attrs = %{agent_token_id: new_agent_token_id} + changeset = AgentAssignment.changeset(existing_assignment, attrs) + + assert changeset.valid? + assert changeset.changes.agent_token_id == new_agent_token_id + end + + test "casts all supported fields" do + agent_token_id = Ecto.UUID.generate() + device_id = Ecto.UUID.generate() + + attrs = %{ + agent_token_id: agent_token_id, + device_id: device_id, + enabled: false + } + + changeset = AgentAssignment.changeset(%AgentAssignment{}, attrs) + + assert changeset.valid? + assert Map.has_key?(changeset.changes, :agent_token_id) + assert Map.has_key?(changeset.changes, :device_id) + assert Map.has_key?(changeset.changes, :enabled) + end + end +end diff --git a/test/towerops/agents/agent_token_test.exs b/test/towerops/agents/agent_token_test.exs new file mode 100644 index 00000000..962b612a --- /dev/null +++ b/test/towerops/agents/agent_token_test.exs @@ -0,0 +1,228 @@ +defmodule Towerops.Agents.AgentTokenTest do + use Towerops.DataCase, async: true + + alias Towerops.Agents.AgentToken + + describe "build_token/3" do + test "generates a token with 48 bytes of entropy" do + org_id = Ecto.UUID.generate() + {token, _changeset} = AgentToken.build_token(org_id, "Test Agent") + + # Decode the base64-encoded token + decoded = Base.url_decode64!(token, padding: false) + + # Should be exactly 48 bytes + assert byte_size(decoded) == 48 + end + + test "returns token string and valid changeset for organization agent" do + org_id = Ecto.UUID.generate() + {token, changeset} = AgentToken.build_token(org_id, "My Agent") + + assert is_binary(token) + assert changeset.valid? + assert changeset.changes.token == token + assert changeset.changes.name == "My Agent" + assert changeset.changes.organization_id == org_id + assert Ecto.Changeset.get_field(changeset, :is_cloud_poller) == false + end + + test "sets is_cloud_poller to false by default" do + org_id = Ecto.UUID.generate() + {_token, changeset} = AgentToken.build_token(org_id, "Test Agent") + + assert Ecto.Changeset.get_field(changeset, :is_cloud_poller) == false + end + + test "generates unique tokens on each call" do + org_id = Ecto.UUID.generate() + {token1, _} = AgentToken.build_token(org_id, "Agent 1") + {token2, _} = AgentToken.build_token(org_id, "Agent 2") + {token3, _} = AgentToken.build_token(org_id, "Agent 3") + + # All tokens should be different + assert token1 != token2 + assert token2 != token3 + assert token1 != token3 + end + + test "creates cloud poller token when is_cloud_poller is true" do + {token, changeset} = AgentToken.build_token(nil, "Cloud Poller", is_cloud_poller: true) + + assert is_binary(token) + assert changeset.valid? + assert changeset.changes.is_cloud_poller == true + refute Map.has_key?(changeset.changes, :organization_id) + end + + test "requires name" do + org_id = Ecto.UUID.generate() + {_token, changeset} = AgentToken.build_token(org_id, nil) + + refute changeset.valid? + assert %{name: ["can't be blank"]} = errors_on(changeset) + end + + test "adds error if cloud poller has organization_id" do + org_id = Ecto.UUID.generate() + {_token, changeset} = AgentToken.build_token(org_id, "Test", is_cloud_poller: true) + + refute changeset.valid? + assert %{organization_id: ["must be nil for cloud pollers"]} = errors_on(changeset) + end + + test "adds error if non-cloud poller has nil organization_id" do + {_token, changeset} = AgentToken.build_token(nil, "Test", is_cloud_poller: false) + + refute changeset.valid? + assert %{organization_id: ["can't be blank for non-cloud pollers"]} = errors_on(changeset) + end + + test "token is URL-safe base64 encoded without padding" do + org_id = Ecto.UUID.generate() + {token, _changeset} = AgentToken.build_token(org_id, "Test Agent") + + # Should not contain padding characters + refute String.contains?(token, "=") + + # Should only contain URL-safe base64 characters + assert token =~ ~r/^[A-Za-z0-9_-]+$/ + end + end + + describe "update_changeset/2" do + test "allows updating name" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Old Name", + organization_id: Ecto.UUID.generate() + } + + attrs = %{name: "New Name"} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.name == "New Name" + end + + test "allows updating allow_remote_debug" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Test Agent", + organization_id: Ecto.UUID.generate(), + allow_remote_debug: false + } + + attrs = %{allow_remote_debug: true} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.allow_remote_debug == true + end + + test "allows updating both name and allow_remote_debug" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Old Name", + organization_id: Ecto.UUID.generate(), + allow_remote_debug: false + } + + attrs = %{name: "New Name", allow_remote_debug: true} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.name == "New Name" + assert changeset.changes.allow_remote_debug == true + end + + test "requires name" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Old Name", + organization_id: Ecto.UUID.generate() + } + + attrs = %{name: ""} + changeset = AgentToken.update_changeset(agent_token, attrs) + + refute changeset.valid? + assert %{name: ["can't be blank"]} = errors_on(changeset) + end + + test "ignores attempts to update token" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "original-token", + name: "Test Agent", + organization_id: Ecto.UUID.generate() + } + + attrs = %{name: "New Name", token: "hacker-token"} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.name == "New Name" + refute Map.has_key?(changeset.changes, :token) + end + + test "ignores attempts to update organization_id" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Test Agent", + organization_id: Ecto.UUID.generate() + } + + new_org_id = Ecto.UUID.generate() + attrs = %{name: "New Name", organization_id: new_org_id} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.name == "New Name" + refute Map.has_key?(changeset.changes, :organization_id) + end + + test "ignores attempts to update enabled" do + agent_token = %AgentToken{ + id: Ecto.UUID.generate(), + token: "existing-token", + name: "Test Agent", + organization_id: Ecto.UUID.generate(), + enabled: true + } + + attrs = %{name: "New Name", enabled: false} + changeset = AgentToken.update_changeset(agent_token, attrs) + + assert changeset.valid? + assert changeset.changes.name == "New Name" + refute Map.has_key?(changeset.changes, :enabled) + end + end + + describe "verify_token/1" do + test "returns {:ok, token} for valid binary token" do + token = "valid-token-string" + assert AgentToken.verify_token(token) == {:ok, token} + end + + test "returns {:ok, token} for any binary string" do + assert AgentToken.verify_token("abc123") == {:ok, "abc123"} + assert AgentToken.verify_token("") == {:ok, ""} + assert AgentToken.verify_token("with-dashes") == {:ok, "with-dashes"} + end + + test "returns {:error, :invalid_token} for non-binary values" do + assert AgentToken.verify_token(nil) == {:error, :invalid_token} + assert AgentToken.verify_token(12_345) == {:error, :invalid_token} + assert AgentToken.verify_token([]) == {:error, :invalid_token} + assert AgentToken.verify_token(%{}) == {:error, :invalid_token} + assert AgentToken.verify_token(:atom) == {:error, :invalid_token} + end + end +end diff --git a/test/towerops/alerts/alert_test.exs b/test/towerops/alerts/alert_test.exs new file mode 100644 index 00000000..0b1edd81 --- /dev/null +++ b/test/towerops/alerts/alert_test.exs @@ -0,0 +1,178 @@ +defmodule Towerops.Alerts.AlertTest do + use Towerops.DataCase, async: true + + alias Towerops.Alerts.Alert + + describe "changeset/2" do + test "valid changeset with required fields" do + attrs = %{ + device_id: Ecto.UUID.generate(), + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z] + } + + changeset = Alert.changeset(%Alert{}, attrs) + + assert changeset.valid? + assert changeset.changes.device_id == attrs.device_id + assert changeset.changes.alert_type == :device_down + assert changeset.changes.triggered_at == attrs.triggered_at + end + + test "valid changeset with all fields" do + device_id = Ecto.UUID.generate() + user_id = Ecto.UUID.generate() + triggered_at = ~U[2026-01-15 10:00:00Z] + acknowledged_at = ~U[2026-01-15 10:05:00Z] + resolved_at = ~U[2026-01-15 10:10:00Z] + email_sent_at = ~U[2026-01-15 10:01:00Z] + + attrs = %{ + device_id: device_id, + alert_type: :device_up, + triggered_at: triggered_at, + acknowledged_at: acknowledged_at, + acknowledged_by_id: user_id, + resolved_at: resolved_at, + email_sent_at: email_sent_at, + message: "Device came back online" + } + + changeset = Alert.changeset(%Alert{}, attrs) + + assert changeset.valid? + assert changeset.changes.device_id == device_id + assert changeset.changes.alert_type == :device_up + assert changeset.changes.triggered_at == triggered_at + assert changeset.changes.acknowledged_at == acknowledged_at + assert changeset.changes.acknowledged_by_id == user_id + assert changeset.changes.resolved_at == resolved_at + assert changeset.changes.email_sent_at == email_sent_at + assert changeset.changes.message == "Device came back online" + end + + test "requires device_id" do + attrs = %{ + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z] + } + + changeset = Alert.changeset(%Alert{}, attrs) + + refute changeset.valid? + assert %{device_id: ["can't be blank"]} = errors_on(changeset) + end + + test "requires alert_type" do + attrs = %{ + device_id: Ecto.UUID.generate(), + triggered_at: ~U[2026-01-15 10:00:00Z] + } + + changeset = Alert.changeset(%Alert{}, attrs) + + refute changeset.valid? + assert %{alert_type: ["can't be blank"]} = errors_on(changeset) + end + + test "requires triggered_at" do + attrs = %{ + device_id: Ecto.UUID.generate(), + alert_type: :device_down + } + + changeset = Alert.changeset(%Alert{}, attrs) + + refute changeset.valid? + assert %{triggered_at: ["can't be blank"]} = errors_on(changeset) + end + + test "validates alert_type is one of valid enum values" do + attrs = %{ + device_id: Ecto.UUID.generate(), + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z] + } + + changeset = Alert.changeset(%Alert{}, attrs) + assert changeset.valid? + + attrs2 = %{attrs | alert_type: :device_up} + changeset2 = Alert.changeset(%Alert{}, attrs2) + assert changeset2.valid? + end + + test "accepts nil for optional fields" do + attrs = %{ + device_id: Ecto.UUID.generate(), + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z], + acknowledged_at: nil, + acknowledged_by_id: nil, + resolved_at: nil, + email_sent_at: nil, + message: nil + } + + changeset = Alert.changeset(%Alert{}, attrs) + + assert changeset.valid? + end + + test "allows updating an existing alert" do + device_id = Ecto.UUID.generate() + + existing_alert = %Alert{ + id: Ecto.UUID.generate(), + device_id: device_id, + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z] + } + + user_id = Ecto.UUID.generate() + + attrs = %{ + acknowledged_at: ~U[2026-01-15 10:05:00Z], + acknowledged_by_id: user_id, + message: "Acknowledged by admin" + } + + changeset = Alert.changeset(existing_alert, attrs) + + assert changeset.valid? + assert changeset.changes.acknowledged_at == attrs.acknowledged_at + assert changeset.changes.acknowledged_by_id == user_id + assert changeset.changes.message == "Acknowledged by admin" + end + + test "casts all supported fields" do + device_id = Ecto.UUID.generate() + user_id = Ecto.UUID.generate() + + attrs = %{ + device_id: device_id, + alert_type: :device_down, + triggered_at: ~U[2026-01-15 10:00:00Z], + acknowledged_at: ~U[2026-01-15 10:05:00Z], + acknowledged_by_id: user_id, + resolved_at: ~U[2026-01-15 10:10:00Z], + email_sent_at: ~U[2026-01-15 10:01:00Z], + message: "Test message" + } + + changeset = Alert.changeset(%Alert{}, attrs) + + assert changeset.valid? + + # All fields should be in changes + assert Map.has_key?(changeset.changes, :device_id) + assert Map.has_key?(changeset.changes, :alert_type) + assert Map.has_key?(changeset.changes, :triggered_at) + assert Map.has_key?(changeset.changes, :acknowledged_at) + assert Map.has_key?(changeset.changes, :acknowledged_by_id) + assert Map.has_key?(changeset.changes, :resolved_at) + assert Map.has_key?(changeset.changes, :email_sent_at) + assert Map.has_key?(changeset.changes, :message) + end + end +end