towerops/test/towerops_web/channels/mobile_channel_test.exs
Graham McIntire 7251930404
add mobile app websocket channel and improve QR linking UX
- add MobileSocket for token-authenticated mobile WebSocket connections
- add MobileChannel with org-level and device-level real-time events
- add /mobile/socket endpoint for iOS app connections
- add "Link Mobile App" to user dropdown menu for discoverability
- improve QR code page with numbered steps and clearer instructions
- add socket/channel tests (17 total)
2026-03-13 17:28:36 -05:00

209 lines
6 KiB
Elixir

defmodule ToweropsWeb.MobileChannelTest do
use Towerops.DataCase, async: false
import Phoenix.ChannelTest
alias Towerops.MobileSessions
alias ToweropsWeb.MobileSocket
@endpoint ToweropsWeb.Endpoint
setup do
user = Towerops.AccountsFixtures.user_fixture()
organization = Towerops.OrganizationsFixtures.organization_fixture(user.id)
device =
Towerops.DevicesFixtures.device_fixture(%{
organization_id: organization.id,
snmp_version: "2c",
snmp_community: "public"
})
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
{:ok, session} =
MobileSessions.complete_qr_login(qr_token.token, %{
device_name: "Test iPhone",
device_os: "iOS 18.0",
app_version: "1.0.0"
})
{:ok, socket} = connect(MobileSocket, %{"token" => session.raw_token})
%{socket: socket, user: user, organization: organization, device: device}
end
# --- Org-level join ---
describe "join mobile:org:* topic" do
test "joins successfully with valid org access", %{socket: socket, organization: org} do
assert {:ok, _reply, _socket} =
subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
end
test "rejects join for unauthorized org", %{socket: socket} do
fake_org_id = Ecto.UUID.generate()
assert {:error, %{reason: "unauthorized"}} =
subscribe_and_join(socket, "mobile:org:#{fake_org_id}", %{})
end
end
# --- Org-level alert events ---
describe "org-level alert events" do
test "pushes alert:changed when alert broadcast received", %{
socket: socket,
organization: org
} do
{:ok, _reply, _socket} = subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"organization:#{org.id}:alerts",
{:alert_changed, org.id}
)
assert_push "alert:changed", %{organization_id: _}
end
end
# --- Org-level device events ---
describe "org-level device events" do
test "pushes device:changed on device_status_changed", %{
socket: socket,
organization: org
} do
{:ok, _reply, _socket} = subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"devices:org:#{org.id}",
{:device_status_changed, org.id}
)
assert_push "device:changed", %{event: "device_status_changed"}
end
test "pushes device:changed on device_created", %{socket: socket, organization: org} do
{:ok, _reply, _socket} = subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"devices:org:#{org.id}",
{:device_created, org.id}
)
assert_push "device:changed", %{event: "device_created"}
end
test "pushes device:changed on device_updated", %{socket: socket, organization: org} do
{:ok, _reply, _socket} = subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"devices:org:#{org.id}",
{:device_updated, org.id}
)
assert_push "device:changed", %{event: "device_updated"}
end
test "pushes device:changed on device_deleted", %{socket: socket, organization: org} do
{:ok, _reply, _socket} = subscribe_and_join(socket, "mobile:org:#{org.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"devices:org:#{org.id}",
{:device_deleted, org.id}
)
assert_push "device:changed", %{event: "device_deleted"}
end
end
# --- Device-level join ---
describe "join mobile:device:* topic" do
test "joins successfully for device in user's org", %{socket: socket, device: device} do
assert {:ok, _reply, _socket} =
subscribe_and_join(socket, "mobile:device:#{device.id}", %{})
end
test "rejects join for non-existent device", %{socket: socket} do
assert {:error, %{reason: "not_found"}} =
subscribe_and_join(socket, "mobile:device:#{Ecto.UUID.generate()}", %{})
end
end
# --- Device-level sensor events ---
describe "device-level sensor events" do
test "pushes device:event for sensor threshold change", %{
socket: socket,
device: device
} do
{:ok, _reply, _socket} =
subscribe_and_join(socket, "mobile:device:#{device.id}", %{})
event = %{
device_id: device.id,
event_type: "sensor_threshold_critical",
severity: "critical",
message: "CPU temperature exceeded 90C",
metadata: %{
sensor_name: "CPU Temp",
sensor_type: "temperature",
current_value: 95.0,
threshold_value: 90.0
},
occurred_at: DateTime.utc_now()
}
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:device_event, event}
)
assert_push "device:event", payload
assert payload.event_type == "sensor_threshold_critical"
assert payload.severity == "critical"
assert payload.device_id == device.id
end
end
# --- Device-level bulk updates ---
describe "device-level bulk updates" do
test "pushes device:updated for sensors_updated", %{socket: socket, device: device} do
{:ok, _reply, _socket} =
subscribe_and_join(socket, "mobile:device:#{device.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:sensors_updated, device.id}
)
assert_push "device:updated", %{
device_id: _,
update_type: "sensors_updated"
}
end
test "pushes device:updated for interfaces_updated", %{socket: socket, device: device} do
{:ok, _reply, _socket} =
subscribe_and_join(socket, "mobile:device:#{device.id}", %{})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:interfaces_updated, device.id}
)
assert_push "device:updated", %{update_type: "interfaces_updated"}
end
end
end