more tests

This commit is contained in:
Graham McIntire 2026-02-03 12:03:54 -06:00
parent b78a40555c
commit 88d549cd92
No known key found for this signature in database
5 changed files with 546 additions and 6 deletions

View file

@ -48,9 +48,6 @@ defmodule Towerops.Accounts.UserConsent do
|> validate_required([:user_id, :consent_type, :version, :granted_at])
|> validate_inclusion(:consent_type, @consent_types)
|> foreign_key_constraint(:user_id)
|> unique_constraint([:user_id, :consent_type, :version],
name: :user_consents_user_id_consent_type_version_index
)
end
@doc """
@ -64,7 +61,8 @@ defmodule Towerops.Accounts.UserConsent do
end
defp validate_not_already_revoked(changeset) do
if get_field(changeset, :revoked_at) do
# Check the original struct's revoked_at, not the changeset's pending changes
if changeset.data.revoked_at do
add_error(changeset, :revoked_at, "consent already revoked")
else
changeset

View file

@ -243,12 +243,16 @@ defmodule ToweropsWeb.UserRegistrationLive do
/>
</div>
<div class="ml-3 text-sm">
<label for="privacy_policy_consent" class="text-zinc-700 dark:text-zinc-300">
<label
for="privacy_policy_consent"
class="text-zinc-700 dark:text-zinc-300 cursor-pointer"
>
I agree to the
<.link
href={~p"/privacy"}
target="_blank"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onclick="event.stopPropagation()"
>
Privacy Policy
</.link>
@ -268,12 +272,16 @@ defmodule ToweropsWeb.UserRegistrationLive do
/>
</div>
<div class="ml-3 text-sm">
<label for="terms_of_service_consent" class="text-zinc-700 dark:text-zinc-300">
<label
for="terms_of_service_consent"
class="text-zinc-700 dark:text-zinc-300 cursor-pointer"
>
I agree to the
<.link
href={~p"/terms"}
target="_blank"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onclick="event.stopPropagation()"
>
Terms of Service
</.link>

View file

@ -0,0 +1,236 @@
defmodule Towerops.Accounts.UserConsentTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
alias Towerops.Accounts.UserConsent
describe "consent_types/0" do
test "returns list of valid consent types" do
types = UserConsent.consent_types()
assert "privacy_policy" in types
assert "terms_of_service" in types
assert length(types) == 2
end
end
describe "current_version/1" do
test "returns current version for privacy_policy" do
assert UserConsent.current_version("privacy_policy") == "1.0"
end
test "returns current version for terms_of_service" do
assert UserConsent.current_version("terms_of_service") == "1.0"
end
end
describe "grant_changeset/2" do
setup do
user = user_fixture()
{:ok, user: user}
end
test "valid attributes create valid changeset", %{user: user} do
attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
assert changeset.valid?
end
test "requires user_id" do
attrs = %{
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).user_id
end
test "requires consent_type", %{user: user} do
attrs = %{
user_id: user.id,
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).consent_type
end
test "requires version", %{user: user} do
attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).version
end
test "requires granted_at", %{user: user} do
attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0"
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).granted_at
end
test "validates consent_type is in valid list", %{user: user} do
attrs = %{
user_id: user.id,
consent_type: "invalid_type",
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
refute changeset.valid?
assert "is invalid" in errors_on(changeset).consent_type
end
test "validates foreign key constraint on user_id" do
attrs = %{
user_id: Ecto.UUID.generate(),
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset = UserConsent.grant_changeset(%UserConsent{}, attrs)
assert changeset.valid?
{:error, changeset} = Repo.insert(changeset)
assert "does not exist" in errors_on(changeset).user_id
end
test "allows multiple consents for same user_id + consent_type + version", %{user: user} do
attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
# Insert first consent
changeset1 = UserConsent.grant_changeset(%UserConsent{}, attrs)
assert {:ok, _consent1} = Repo.insert(changeset1)
# Insert another consent (allowed - e.g., user re-consents after revoking)
changeset2 = UserConsent.grant_changeset(%UserConsent{}, attrs)
assert {:ok, _consent2} = Repo.insert(changeset2)
end
test "allows same user to consent to different types", %{user: user} do
privacy_attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
terms_attrs = %{
user_id: user.id,
consent_type: "terms_of_service",
version: "1.0",
granted_at: DateTime.utc_now()
}
changeset1 = UserConsent.grant_changeset(%UserConsent{}, privacy_attrs)
assert {:ok, _consent1} = Repo.insert(changeset1)
changeset2 = UserConsent.grant_changeset(%UserConsent{}, terms_attrs)
assert {:ok, _consent2} = Repo.insert(changeset2)
end
test "allows same user to consent to different versions", %{user: user} do
v1_attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0",
granted_at: DateTime.utc_now()
}
v2_attrs = %{
user_id: user.id,
consent_type: "privacy_policy",
version: "2.0",
granted_at: DateTime.utc_now()
}
changeset1 = UserConsent.grant_changeset(%UserConsent{}, v1_attrs)
assert {:ok, _consent1} = Repo.insert(changeset1)
changeset2 = UserConsent.grant_changeset(%UserConsent{}, v2_attrs)
assert {:ok, _consent2} = Repo.insert(changeset2)
end
end
describe "revoke_changeset/2" do
setup do
user = user_fixture()
granted_at = DateTime.utc_now()
consent =
%UserConsent{}
|> UserConsent.grant_changeset(%{
user_id: user.id,
consent_type: "privacy_policy",
version: "1.0",
granted_at: granted_at
})
|> Repo.insert!()
{:ok, user: user, consent: consent}
end
test "valid attributes create valid changeset", %{consent: consent} do
attrs = %{revoked_at: DateTime.utc_now()}
changeset = UserConsent.revoke_changeset(consent, attrs)
assert changeset.valid?
end
test "requires revoked_at", %{consent: consent} do
changeset = UserConsent.revoke_changeset(consent, %{})
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).revoked_at
end
test "prevents revoking already revoked consent", %{consent: consent} do
# Revoke once
revoked_at = DateTime.utc_now()
{:ok, revoked_consent} =
consent
|> UserConsent.revoke_changeset(%{revoked_at: revoked_at})
|> Repo.update()
# Try to revoke again
changeset = UserConsent.revoke_changeset(revoked_consent, %{revoked_at: DateTime.utc_now()})
refute changeset.valid?
assert "consent already revoked" in errors_on(changeset).revoked_at
end
test "successfully revokes consent", %{consent: consent} do
revoked_at = DateTime.truncate(DateTime.utc_now(), :second)
changeset = UserConsent.revoke_changeset(consent, %{revoked_at: revoked_at})
assert {:ok, updated_consent} = Repo.update(changeset)
assert updated_consent.revoked_at == revoked_at
end
end
end

View file

@ -1,5 +1,6 @@
defmodule Towerops.Workers.AgentLatencyEvaluatorTest do
use Towerops.DataCase, async: false
use Oban.Testing, repo: Towerops.Repo
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
@ -10,6 +11,10 @@ defmodule Towerops.Workers.AgentLatencyEvaluatorTest do
alias Towerops.Workers.AgentLatencyEvaluator
describe "perform/1" do
test "returns :ok when no reassignment candidates exist" do
assert :ok = perform_job(AgentLatencyEvaluator, %{})
end
test "reassigns device to faster agent when 20%+ improvement available" do
user = user_fixture()
org = organization_fixture(user.id)
@ -440,5 +445,153 @@ defmodule Towerops.Workers.AgentLatencyEvaluatorTest do
assert updated_site1.agent_token_id == fast_agent.id
assert updated_site2.agent_token_id == fast_agent.id
end
test "broadcasts PubSub event when device is reassigned" do
user = user_fixture()
org = organization_fixture(user.id)
{:ok, slow_agent, _} = Agents.create_agent_token(org.id, "Slow Agent")
{:ok, fast_agent, _} = Agents.create_agent_token(org.id, "Fast Agent")
{:ok, site} =
Sites.create_site(%{
name: "Test Site",
organization_id: org.id,
agent_token_id: slow_agent.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public",
snmp_version: "2c"
})
# Create checks showing significant improvement
for _ <- 1..15 do
Monitoring.create_check(%{
device_id: device.id,
agent_token_id: slow_agent.id,
status: :success,
response_time_ms: 100,
checked_at: DateTime.utc_now()
})
Monitoring.create_check(%{
device_id: device.id,
agent_token_id: fast_agent.id,
status: :success,
response_time_ms: 50,
checked_at: DateTime.utc_now()
})
end
# Subscribe to PubSub
Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:latency")
# Trigger evaluation
assert :ok = perform_job(AgentLatencyEvaluator, %{})
# Verify PubSub broadcast was sent
assert_received {:device_reassigned, candidate}
assert candidate.device_id == device.id
assert candidate.best_agent_token_id == fast_agent.id
assert candidate.current_agent_token_id == slow_agent.id
assert candidate.improvement_percent >= 20
end
test "does not broadcast when no reassignment occurs" do
user = user_fixture()
org = organization_fixture(user.id)
{:ok, agent1, _} = Agents.create_agent_token(org.id, "Agent 1")
{:ok, site} =
Sites.create_site(%{
name: "Test Site",
organization_id: org.id,
agent_token_id: agent1.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public",
snmp_version: "2c"
})
# Create checks with only one agent (no alternative)
for _ <- 1..15 do
Monitoring.create_check(%{
device_id: device.id,
agent_token_id: agent1.id,
status: :success,
response_time_ms: 100,
checked_at: DateTime.utc_now()
})
end
# Subscribe to PubSub
Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:latency")
# Trigger evaluation
assert :ok = perform_job(AgentLatencyEvaluator, %{})
# Verify no broadcast was sent
refute_received {:device_reassigned, _}
end
test "handles reassignment failures gracefully" do
user = user_fixture()
org = organization_fixture(user.id)
{:ok, slow_agent, _} = Agents.create_agent_token(org.id, "Slow Agent")
{:ok, fast_agent, _} = Agents.create_agent_token(org.id, "Fast Agent")
{:ok, site} =
Sites.create_site(%{
name: "Test Site",
organization_id: org.id,
agent_token_id: slow_agent.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public",
snmp_version: "2c"
})
# Create checks showing improvement
for _ <- 1..15 do
Monitoring.create_check(%{
device_id: device.id,
agent_token_id: slow_agent.id,
status: :success,
response_time_ms: 100,
checked_at: DateTime.utc_now()
})
Monitoring.create_check(%{
device_id: device.id,
agent_token_id: fast_agent.id,
status: :success,
response_time_ms: 50,
checked_at: DateTime.utc_now()
})
end
# Delete the site to cause a failure during reassignment
Sites.delete_site(site)
# Worker should complete successfully despite the error
assert :ok = perform_job(AgentLatencyEvaluator, %{})
end
end
end

View file

@ -1,5 +1,6 @@
defmodule Towerops.Workers.StaleAgentWorkerTest do
use Towerops.DataCase, async: false
use Oban.Testing, repo: Towerops.Repo
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
@ -8,6 +9,150 @@ defmodule Towerops.Workers.StaleAgentWorkerTest do
alias Towerops.Agents.AgentToken
alias Towerops.Workers.StaleAgentWorker
describe "perform/1" do
setup do
user = user_fixture()
organization = organization_fixture(user.id)
{:ok, organization: organization}
end
test "returns :ok when no stale agents exist" do
assert :ok = perform_job(StaleAgentWorker, %{})
end
test "returns :ok when all agents are fresh", %{organization: org} do
{:ok, agent, _token} = Agents.create_agent_token(org.id, "Fresh Agent")
# Update heartbeat to now
Agents.update_agent_token_heartbeat(agent.id, "192.168.1.1", %{})
assert :ok = perform_job(StaleAgentWorker, %{})
end
test "processes stale agents and broadcasts PubSub event", %{organization: org} do
{:ok, agent, _token} = Agents.create_agent_token(org.id, "Stale Agent")
# Set last_seen_at to 15 minutes ago
stale_time = DateTime.add(DateTime.utc_now(), -15, :minute)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^agent.id),
set: [last_seen_at: stale_time]
)
# Subscribe to PubSub to verify broadcast
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
# Verify PubSub broadcast was sent
assert_received {:agents_stale, stale_agents}
assert length(stale_agents) == 1
assert hd(stale_agents).id == agent.id
end
test "broadcasts newly stale agents (10-15 minutes)", %{organization: org} do
{:ok, agent, _token} = Agents.create_agent_token(org.id, "Newly Stale")
# Set last_seen_at to 12 minutes ago (within newly stale window)
stale_time = DateTime.add(DateTime.utc_now(), -12, :minute)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^agent.id),
set: [last_seen_at: stale_time]
)
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
assert_received {:agents_stale, stale_agents}
assert length(stale_agents) == 1
assert hd(stale_agents).id == agent.id
assert hd(stale_agents).organization_id == org.id
end
test "broadcasts long-term stale agents (>15 minutes)", %{organization: org} do
{:ok, agent, _token} = Agents.create_agent_token(org.id, "Long-term Stale")
# Set last_seen_at to 30 minutes ago
stale_time = DateTime.add(DateTime.utc_now(), -30, :minute)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^agent.id),
set: [last_seen_at: stale_time]
)
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
assert_received {:agents_stale, stale_agents}
assert length(stale_agents) == 1
assert hd(stale_agents).id == agent.id
end
test "handles mix of newly stale and long-term stale agents", %{organization: org} do
{:ok, newly_stale, _} = Agents.create_agent_token(org.id, "Newly Stale")
{:ok, long_term_stale, _} = Agents.create_agent_token(org.id, "Long-term Stale")
# Newly stale: 12 minutes ago
newly_stale_time = DateTime.add(DateTime.utc_now(), -12, :minute)
# Long-term stale: 20 minutes ago
long_term_time = DateTime.add(DateTime.utc_now(), -20, :minute)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^newly_stale.id),
set: [last_seen_at: newly_stale_time]
)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^long_term_stale.id),
set: [last_seen_at: long_term_time]
)
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
# Verify both agents are included in PubSub broadcast
assert_received {:agents_stale, stale_agents}
assert length(stale_agents) == 2
stale_ids = Enum.map(stale_agents, & &1.id)
assert newly_stale.id in stale_ids
assert long_term_stale.id in stale_ids
end
test "does not broadcast when no stale agents exist" do
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
refute_received {:agents_stale, _}
end
test "preloads organization association for stale agents", %{organization: org} do
{:ok, agent, _token} = Agents.create_agent_token(org.id, "Stale Agent")
stale_time = DateTime.add(DateTime.utc_now(), -15, :minute)
Towerops.Repo.update_all(
from(a in AgentToken, where: a.id == ^agent.id),
set: [last_seen_at: stale_time]
)
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
assert :ok = perform_job(StaleAgentWorker, %{})
assert_received {:agents_stale, [stale_agent]}
# Verify organization is preloaded
assert %Ecto.Association.NotLoaded{} != stale_agent.organization
assert stale_agent.organization.id == org.id
end
end
describe "find_stale_agents/0" do
setup do
user = user_fixture()