defmodule ToweropsWeb.Api.V1.ChecksControllerTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures import Towerops.OrganizationsFixtures alias Towerops.ApiTokens alias Towerops.Monitoring setup do user = user_fixture() organization = organization_fixture(user.id) {:ok, {_token, raw_token}} = ApiTokens.create_api_token(%{ organization_id: organization.id, user_id: user.id, name: "Test Token" }) 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 defp create_check(organization_id, attrs \\ %{}) do defaults = %{ name: "Test Check", check_type: "http", organization_id: organization_id, config: %{"url" => "https://example.com"} } {:ok, check} = Monitoring.create_check(Map.merge(defaults, attrs)) check end describe "index/2" do test "lists service checks for authenticated organization", %{ conn: conn, organization: organization } do check1 = create_check(organization.id, %{name: "HTTP Check", check_type: "http", config: %{"url" => "https://example.com"}}) check2 = create_check(organization.id, %{ name: "TCP Check", check_type: "tcp", config: %{"host" => "10.0.0.1", "port" => 80} }) conn = get(conn, ~p"/api/v1/checks") assert %{"checks" => checks} = json_response(conn, 200) assert length(checks) == 2 check_ids = Enum.map(checks, & &1["id"]) assert check1.id in check_ids assert check2.id in check_ids check_json = List.first(checks) assert Map.has_key?(check_json, "id") assert Map.has_key?(check_json, "name") assert Map.has_key?(check_json, "check_type") assert Map.has_key?(check_json, "config") assert Map.has_key?(check_json, "enabled") assert Map.has_key?(check_json, "inserted_at") end test "excludes SNMP checks from listing", %{conn: conn, organization: organization} do _http = create_check(organization.id, %{name: "HTTP Check", check_type: "http", config: %{"url" => "https://example.com"}}) # Create an SNMP check — should not appear {:ok, _snmp} = Monitoring.create_check(%{ name: "SNMP Sensor", check_type: "snmp_sensor", organization_id: organization.id, config: %{"oid" => "1.3.6.1.2.1.1.3.0"} }) conn = get(conn, ~p"/api/v1/checks") assert %{"checks" => checks} = json_response(conn, 200) assert length(checks) == 1 assert List.first(checks)["check_type"] == "http" end test "filters by check_type query param", %{conn: conn, organization: organization} do _http = create_check(organization.id, %{name: "HTTP Check", check_type: "http", config: %{"url" => "https://example.com"}}) _tcp = create_check(organization.id, %{ name: "TCP Check", check_type: "tcp", config: %{"host" => "10.0.0.1", "port" => 80} }) conn = get(conn, ~p"/api/v1/checks?check_type=tcp") assert %{"checks" => checks} = json_response(conn, 200) assert length(checks) == 1 assert List.first(checks)["check_type"] == "tcp" end test "returns empty list when organization has no checks", %{conn: conn} do conn = get(conn, ~p"/api/v1/checks") assert %{"checks" => []} = json_response(conn, 200) end test "does not return checks from other organizations", %{conn: conn} do other_user = user_fixture() other_org = organization_fixture(other_user.id) _other_check = create_check(other_org.id) conn = get(conn, ~p"/api/v1/checks") assert %{"checks" => checks} = json_response(conn, 200) assert Enum.empty?(checks) end end describe "create/2" do test "creates HTTP check with valid attributes", %{conn: conn, organization: organization} do params = %{ "check" => %{ "name" => "Web Health", "check_type" => "http", "config" => %{ "url" => "https://example.com/health", "expected_status" => 200 } } } conn = post(conn, ~p"/api/v1/checks", params) assert %{ "id" => id, "name" => "Web Health", "check_type" => "http", "enabled" => true } = json_response(conn, 201) assert is_binary(id) check = Monitoring.get_check!(id) assert check.organization_id == organization.id end test "creates TCP check with valid attributes", %{conn: conn} do params = %{ "check" => %{ "name" => "MySQL Port", "check_type" => "tcp", "config" => %{ "host" => "10.0.0.5", "port" => 3306 } } } conn = post(conn, ~p"/api/v1/checks", params) assert %{ "id" => _id, "name" => "MySQL Port", "check_type" => "tcp" } = json_response(conn, 201) end test "creates DNS check with valid attributes", %{conn: conn} do params = %{ "check" => %{ "name" => "DNS Resolution", "check_type" => "dns", "config" => %{ "hostname" => "google.com", "dns_server" => "8.8.8.8", "record_type" => "A" } } } conn = post(conn, ~p"/api/v1/checks", params) assert %{ "id" => _id, "name" => "DNS Resolution", "check_type" => "dns" } = json_response(conn, 201) end test "creates ping check with valid attributes", %{conn: conn} do params = %{ "check" => %{ "name" => "Gateway Ping", "check_type" => "ping", "config" => %{ "host" => "10.0.0.1", "count" => 3 } } } conn = post(conn, ~p"/api/v1/checks", params) assert %{ "id" => _id, "name" => "Gateway Ping", "check_type" => "ping" } = json_response(conn, 201) end test "returns 422 when config is invalid for check type", %{conn: conn} do params = %{ "check" => %{ "name" => "Bad HTTP Check", "check_type" => "http", "config" => %{} } } conn = post(conn, ~p"/api/v1/checks", params) assert %{"errors" => _errors} = json_response(conn, 422) end test "returns 422 when name is missing", %{conn: conn} do params = %{ "check" => %{ "check_type" => "http", "config" => %{"url" => "https://example.com"} } } conn = post(conn, ~p"/api/v1/checks", params) assert %{"errors" => errors} = json_response(conn, 422) assert Map.has_key?(errors, "name") end test "returns 400 when check parameter is missing", %{conn: conn} do conn = post(conn, ~p"/api/v1/checks", %{}) assert %{"error" => "Missing 'check' parameter"} = json_response(conn, 400) end test "rejects SNMP check types", %{conn: conn} do params = %{ "check" => %{ "name" => "SNMP Check", "check_type" => "snmp_sensor", "config" => %{"oid" => "1.3.6.1.2.1.1.3.0"} } } conn = post(conn, ~p"/api/v1/checks", params) assert %{"error" => _} = json_response(conn, 400) end end describe "show/2" do test "returns check when it belongs to organization", %{ conn: conn, organization: organization } do check = create_check(organization.id, %{name: "Show Me"}) conn = get(conn, ~p"/api/v1/checks/#{check.id}") assert %{ "id" => id, "name" => "Show Me", "check_type" => "http" } = json_response(conn, 200) assert id == check.id end test "returns 404 when check does not exist", %{conn: conn} do conn = get(conn, ~p"/api/v1/checks/#{Ecto.UUID.generate()}") assert %{"error" => "Check not found"} = json_response(conn, 404) end test "returns 403 when check belongs to different organization", %{conn: conn} do other_user = user_fixture() other_org = organization_fixture(other_user.id) other_check = create_check(other_org.id) conn = get(conn, ~p"/api/v1/checks/#{other_check.id}") assert %{"error" => "Access denied to this check"} = json_response(conn, 403) end end describe "update/2" do test "updates check with valid attributes", %{conn: conn, organization: organization} do check = create_check(organization.id, %{name: "Original"}) params = %{ "check" => %{ "name" => "Updated", "interval_seconds" => 120 } } conn = patch(conn, ~p"/api/v1/checks/#{check.id}", params) assert %{ "id" => id, "name" => "Updated", "interval_seconds" => 120 } = json_response(conn, 200) assert id == check.id end test "returns 404 when check does not exist", %{conn: conn} do params = %{ "check" => %{ "name" => "Updated" } } conn = patch(conn, ~p"/api/v1/checks/#{Ecto.UUID.generate()}", params) assert %{"error" => "Check not found"} = json_response(conn, 404) end test "returns 403 when check belongs to different organization", %{conn: conn} do other_user = user_fixture() other_org = organization_fixture(other_user.id) other_check = create_check(other_org.id) params = %{"check" => %{"name" => "Hacked"}} conn = patch(conn, ~p"/api/v1/checks/#{other_check.id}", params) assert %{"error" => "Access denied to this check"} = json_response(conn, 403) end test "returns 422 with invalid params", %{conn: conn, organization: organization} do check = create_check(organization.id) params = %{"check" => %{"name" => ""}} conn = patch(conn, ~p"/api/v1/checks/#{check.id}", params) assert %{"errors" => errors} = json_response(conn, 422) assert Map.has_key?(errors, "name") end test "returns 400 when check parameter is missing", %{conn: conn, organization: organization} do check = create_check(organization.id) conn = patch(conn, ~p"/api/v1/checks/#{check.id}", %{}) assert %{"error" => "Missing 'check' parameter"} = json_response(conn, 400) end end describe "delete/2" do test "deletes check successfully", %{conn: conn, organization: organization} do check = create_check(organization.id, %{name: "Doomed"}) conn = delete(conn, ~p"/api/v1/checks/#{check.id}") assert %{"success" => true} = json_response(conn, 200) assert Monitoring.get_check(check.id) == nil end test "returns 404 when check does not exist", %{conn: conn} do conn = delete(conn, ~p"/api/v1/checks/#{Ecto.UUID.generate()}") assert %{"error" => "Check not found"} = json_response(conn, 404) end test "returns 403 when check belongs to different organization", %{conn: conn} do other_user = user_fixture() other_org = organization_fixture(other_user.id) other_check = create_check(other_org.id) conn = delete(conn, ~p"/api/v1/checks/#{other_check.id}") assert %{"error" => "Access denied to this check"} = json_response(conn, 403) end end describe "authentication" do test "returns 401 without authorization header" do conn = build_conn() |> put_req_header("accept", "application/json") |> get(~p"/api/v1/checks") assert %{"error" => _message} = json_response(conn, 401) end test "returns 401 with invalid token" do conn = build_conn() |> put_req_header("authorization", "Bearer towerops_invalid_token") |> put_req_header("accept", "application/json") |> get(~p"/api/v1/checks") assert %{"error" => _message} = json_response(conn, 401) end end end