defmodule ToweropsWeb.Api.V1.DevicesControllerTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures import Towerops.DevicesFixtures import Towerops.OrganizationsFixtures alias Towerops.ApiTokens alias Towerops.Sites setup do # Create organization with owner user = user_fixture() organization = organization_fixture(user.id) # Create API token for authentication {:ok, {_token, raw_token}} = ApiTokens.create_api_token(%{ organization_id: organization.id, user_id: user.id, name: "Test Token" }) # Setup authenticated connection conn = build_conn() |> put_req_header("authorization", "Bearer #{raw_token}") |> put_req_header("accept", "application/json") %{ conn: conn, user: user, organization: organization, raw_token: raw_token } end describe "index/2" do test "lists all devices for authenticated organization", %{ conn: conn, organization: organization } do device1 = device_fixture(%{organization_id: organization.id}) device2 = device_fixture(%{organization_id: organization.id}) conn = get(conn, ~p"/api/v1/devices") assert %{"devices" => devices} = json_response(conn, 200) assert length(devices) == 2 device_ids = Enum.map(devices, & &1["id"]) assert device1.id in device_ids assert device2.id in device_ids # Check response structure device_json = List.first(devices) assert Map.has_key?(device_json, "id") assert Map.has_key?(device_json, "name") assert Map.has_key?(device_json, "ip_address") assert Map.has_key?(device_json, "site_id") assert Map.has_key?(device_json, "monitoring_enabled") assert Map.has_key?(device_json, "snmp_enabled") assert Map.has_key?(device_json, "inserted_at") end test "filters devices by site_id when provided", %{ conn: conn, organization: organization } do {:ok, site1} = Sites.create_site(%{name: "Site 1", organization_id: organization.id}) {:ok, site2} = Sites.create_site(%{name: "Site 2", organization_id: organization.id}) device_in_site1 = device_fixture(%{organization_id: organization.id, site_id: site1.id}) device_in_site2 = device_fixture(%{organization_id: organization.id, site_id: site2.id}) _device_no_site = device_fixture(%{organization_id: organization.id}) conn = get(conn, ~p"/api/v1/devices", site_id: site1.id) assert %{"devices" => devices} = json_response(conn, 200) assert length(devices) == 1 assert List.first(devices)["id"] == device_in_site1.id refute device_in_site2.id in Enum.map(devices, & &1["id"]) end test "returns empty list when organization has no devices", %{conn: conn} do conn = get(conn, ~p"/api/v1/devices") assert %{"devices" => []} = json_response(conn, 200) end test "does not return devices from other organizations", %{conn: conn} do # Create another organization with a device other_user = user_fixture() other_org = organization_fixture(other_user.id) _other_device = device_fixture(%{organization_id: other_org.id}) conn = get(conn, ~p"/api/v1/devices") assert %{"devices" => devices} = json_response(conn, 200) assert Enum.empty?(devices) end end describe "create/2" do test "creates device with valid attributes", %{conn: conn, organization: organization} do device_params = %{ "device" => %{ "name" => "Test Router", "ip_address" => "192.168.1.1", "snmp_enabled" => true, "snmp_community" => "public" } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{ "id" => id, "name" => "Test Router", "ip_address" => "192.168.1.1", "organization_id" => org_id, "snmp_enabled" => true } = json_response(conn, 201) assert org_id == organization.id assert is_binary(id) end test "creates device with site_id", %{conn: conn, organization: organization} do {:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: organization.id}) device_params = %{ "device" => %{ "name" => "Test Router", "ip_address" => "192.168.1.1", "site_id" => site.id } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{"id" => _id, "site_id" => site_id} = json_response(conn, 201) assert site_id == site.id end test "defaults organization_id to authenticated organization", %{ conn: conn, organization: organization } do device_params = %{ "device" => %{ "name" => "Test Router", "ip_address" => "192.168.1.1" } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{"organization_id" => org_id} = json_response(conn, 201) assert org_id == organization.id end test "returns 403 when site belongs to different organization", %{conn: conn} do # Create another organization with a site other_user = user_fixture() other_org = organization_fixture(other_user.id) {:ok, other_site} = Sites.create_site(%{name: "Other Site", organization_id: other_org.id}) device_params = %{ "device" => %{ "name" => "Test Router", "ip_address" => "192.168.1.1", "site_id" => other_site.id } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{"error" => "Access denied to this site"} = json_response(conn, 403) end test "returns 403 when site_id does not exist", %{conn: conn} do device_params = %{ "device" => %{ "name" => "Test Router", "ip_address" => "192.168.1.1", "site_id" => Ecto.UUID.generate() } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{"error" => "Site not found"} = json_response(conn, 403) end test "returns 422 with invalid device params", %{conn: conn} do device_params = %{ "device" => %{ "name" => "", "ip_address" => "invalid-ip" } } conn = post(conn, ~p"/api/v1/devices", device_params) assert %{"errors" => errors} = json_response(conn, 422) assert Map.has_key?(errors, "name") or Map.has_key?(errors, "ip_address") end test "returns 400 when device parameter is missing", %{conn: conn} do conn = post(conn, ~p"/api/v1/devices", %{}) assert %{"error" => "Missing 'device' parameter"} = json_response(conn, 400) end end describe "show/2" do test "returns device when it belongs to organization", %{ conn: conn, organization: organization } do device = device_fixture(%{organization_id: organization.id}) conn = get(conn, ~p"/api/v1/devices/#{device.id}") assert %{ "id" => id, "name" => name, "ip_address" => ip, "monitoring_enabled" => _, "snmp_enabled" => _, "snmp_version" => _, "snmp_port" => _, "description" => _, "check_interval_seconds" => _ } = json_response(conn, 200) assert id == device.id assert name == device.name assert ip == device.ip_address end test "returns 404 when device does not exist", %{conn: conn} do conn = get(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}") assert %{"error" => "Device not found"} = json_response(conn, 404) end test "returns 403 when device belongs to different organization", %{conn: conn} do # Create another organization with a device other_user = user_fixture() other_org = organization_fixture(other_user.id) other_device = device_fixture(%{organization_id: other_org.id}) conn = get(conn, ~p"/api/v1/devices/#{other_device.id}") assert %{"error" => "Access denied to this device"} = json_response(conn, 403) end end describe "update/2" do test "updates device with valid attributes", %{conn: conn, organization: organization} do device = device_fixture(%{organization_id: organization.id}) update_params = %{ "device" => %{ "name" => "Updated Router", "ip_address" => "192.168.1.100" } } conn = patch(conn, ~p"/api/v1/devices/#{device.id}", update_params) assert %{ "id" => id, "name" => "Updated Router", "ip_address" => "192.168.1.100" } = json_response(conn, 200) assert id == device.id end test "returns 404 when device does not exist", %{conn: conn} do update_params = %{ "device" => %{ "name" => "Updated Router" } } conn = patch(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}", update_params) assert %{"error" => "Device not found"} = json_response(conn, 404) end test "returns 403 when device belongs to different organization", %{conn: conn} do # Create another organization with a device other_user = user_fixture() other_org = organization_fixture(other_user.id) other_device = device_fixture(%{organization_id: other_org.id}) update_params = %{ "device" => %{ "name" => "Updated Router" } } conn = patch(conn, ~p"/api/v1/devices/#{other_device.id}", update_params) assert %{"error" => "Access denied to this device"} = json_response(conn, 403) end test "returns 422 with invalid update params", %{conn: conn, organization: organization} do device = device_fixture(%{organization_id: organization.id}) update_params = %{ "device" => %{ "name" => "", "ip_address" => "invalid-ip" } } conn = patch(conn, ~p"/api/v1/devices/#{device.id}", update_params) assert %{"errors" => errors} = json_response(conn, 422) assert Map.has_key?(errors, "name") or Map.has_key?(errors, "ip_address") end test "returns 400 when device parameter is missing", %{ conn: conn, organization: organization } do device = device_fixture(%{organization_id: organization.id}) conn = patch(conn, ~p"/api/v1/devices/#{device.id}", %{}) assert %{"error" => "Missing 'device' parameter"} = json_response(conn, 400) end end describe "delete/2" do test "deletes device successfully", %{conn: conn, organization: organization} do device = device_fixture(%{organization_id: organization.id}) conn = delete(conn, ~p"/api/v1/devices/#{device.id}") assert %{"success" => true} = json_response(conn, 200) # Verify device is deleted assert_raise Ecto.NoResultsError, fn -> Towerops.Devices.get_device!(device.id) end end test "returns 404 when device does not exist", %{conn: conn} do conn = delete(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}") assert %{"error" => "Device not found"} = json_response(conn, 404) end test "returns 403 when device belongs to different organization", %{conn: conn} do # Create another organization with a device other_user = user_fixture() other_org = organization_fixture(other_user.id) other_device = device_fixture(%{organization_id: other_org.id}) conn = delete(conn, ~p"/api/v1/devices/#{other_device.id}") assert %{"error" => "Access denied to this device"} = json_response(conn, 403) end end end