add maintenance windows REST API and GraphQL support
REST endpoints for CRUD, scoped filtering (active/upcoming/past). GraphQL queries and mutations for maintenance windows. Fix agent edit test assertion message.
This commit is contained in:
parent
f6e9063577
commit
17fa005781
8 changed files with 600 additions and 1 deletions
|
|
@ -0,0 +1,163 @@
|
|||
defmodule ToweropsWeb.Api.V1.MaintenanceWindowsController do
|
||||
@moduledoc """
|
||||
API controller for managing maintenance windows.
|
||||
|
||||
All endpoints require API token authentication and operations are scoped
|
||||
to the organization associated with the token.
|
||||
"""
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1]
|
||||
|
||||
alias Towerops.Maintenance
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
alias ToweropsWeb.ScopedResource
|
||||
|
||||
@doc "GET /api/v1/maintenance_windows"
|
||||
def index(conn, params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
filter = Map.get(params, "filter")
|
||||
|
||||
opts =
|
||||
case filter do
|
||||
"active" -> [filter: :active]
|
||||
"upcoming" -> [filter: :upcoming]
|
||||
"past" -> [filter: :past]
|
||||
_ -> []
|
||||
end
|
||||
|
||||
windows =
|
||||
organization_id
|
||||
|> Maintenance.list_windows(opts)
|
||||
|> Enum.map(&format_window/1)
|
||||
|
||||
json(conn, %{maintenance_windows: windows})
|
||||
end
|
||||
|
||||
@doc "POST /api/v1/maintenance_windows"
|
||||
def create(conn, %{"maintenance_window" => window_params}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
user_id = conn.assigns.current_user.id
|
||||
|
||||
attrs =
|
||||
window_params
|
||||
|> Map.put("organization_id", organization_id)
|
||||
|> Map.put("created_by_id", user_id)
|
||||
|
||||
case Maintenance.create_window(attrs) do
|
||||
{:ok, window} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(format_window(window))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing 'maintenance_window' parameter"})
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/maintenance_windows/:id"
|
||||
def show(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case ScopedResource.fetch(MaintenanceWindow, id, organization_id) do
|
||||
{:ok, window} ->
|
||||
json(conn, format_window(window))
|
||||
|
||||
{:error, :forbidden} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Access denied to this maintenance window"})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Maintenance window not found"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc "PATCH /api/v1/maintenance_windows/:id"
|
||||
def update(conn, %{"id" => id, "maintenance_window" => window_params}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case ScopedResource.fetch(MaintenanceWindow, id, organization_id) do
|
||||
{:ok, window} ->
|
||||
case Maintenance.update_window(window, window_params) do
|
||||
{:ok, updated} ->
|
||||
json(conn, format_window(updated))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
|
||||
{:error, :forbidden} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Access denied to this maintenance window"})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Maintenance window not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing 'maintenance_window' parameter"})
|
||||
end
|
||||
|
||||
@doc "DELETE /api/v1/maintenance_windows/:id"
|
||||
def delete(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case ScopedResource.fetch(MaintenanceWindow, id, organization_id) do
|
||||
{:ok, window} ->
|
||||
case Maintenance.delete_window(window) do
|
||||
{:ok, _} ->
|
||||
json(conn, %{success: true})
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
|
||||
{:error, :forbidden} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Access denied to this maintenance window"})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Maintenance window not found"})
|
||||
end
|
||||
end
|
||||
|
||||
defp format_window(window) do
|
||||
%{
|
||||
id: window.id,
|
||||
name: window.name,
|
||||
reason: window.reason,
|
||||
starts_at: window.starts_at,
|
||||
ends_at: window.ends_at,
|
||||
recurring: window.recurring,
|
||||
recurrence_rule: window.recurrence_rule,
|
||||
suppress_alerts: window.suppress_alerts,
|
||||
site_id: window.site_id,
|
||||
device_id: window.device_id,
|
||||
inserted_at: window.inserted_at
|
||||
}
|
||||
end
|
||||
end
|
||||
79
lib/towerops_web/graphql/resolvers/maintenance_window.ex
Normal file
79
lib/towerops_web/graphql/resolvers/maintenance_window.ex
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
defmodule ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow do
|
||||
@moduledoc "GraphQL resolvers for maintenance window queries and mutations."
|
||||
|
||||
alias Towerops.Maintenance
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
alias ToweropsWeb.GraphQL.Resolvers.Helpers
|
||||
alias ToweropsWeb.ScopedResource
|
||||
|
||||
def list(_parent, args, %{context: %{organization_id: org_id}}) do
|
||||
opts =
|
||||
case Map.get(args, :filter) do
|
||||
"active" -> [filter: :active]
|
||||
"upcoming" -> [filter: :upcoming]
|
||||
"past" -> [filter: :past]
|
||||
_ -> []
|
||||
end
|
||||
|
||||
{:ok, Maintenance.list_windows(org_id, opts)}
|
||||
end
|
||||
|
||||
def list(_parent, _args, _resolution), do: Helpers.authentication_error()
|
||||
|
||||
def get(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
|
||||
case ScopedResource.fetch(MaintenanceWindow, id, org_id) do
|
||||
{:ok, window} -> {:ok, window}
|
||||
{:error, _} -> {:error, "Maintenance window not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def get(_parent, _args, _resolution), do: Helpers.authentication_error()
|
||||
|
||||
def create(_parent, %{input: input}, %{context: %{organization_id: org_id, user: user}}) do
|
||||
user_id = user.id
|
||||
|
||||
attrs =
|
||||
input
|
||||
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||
|> Map.put("organization_id", org_id)
|
||||
|> Map.put("created_by_id", user_id)
|
||||
|
||||
case Maintenance.create_window(attrs) do
|
||||
{:ok, window} -> {:ok, window}
|
||||
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
def create(_parent, _args, _resolution), do: Helpers.authentication_error()
|
||||
|
||||
def update(_parent, %{id: id, input: input}, %{context: %{organization_id: org_id}}) do
|
||||
with {:ok, window} <- fetch_org_window(id, org_id) do
|
||||
attrs = Map.new(input, fn {k, v} -> {to_string(k), v} end)
|
||||
|
||||
case Maintenance.update_window(window, attrs) do
|
||||
{:ok, updated} -> {:ok, updated}
|
||||
{:error, changeset} -> {:error, Helpers.format_changeset_errors(changeset)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update(_parent, _args, _resolution), do: Helpers.authentication_error()
|
||||
|
||||
def delete(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
|
||||
with {:ok, window} <- fetch_org_window(id, org_id) do
|
||||
case Maintenance.delete_window(window) do
|
||||
{:ok, _} -> {:ok, %{success: true, message: "Maintenance window deleted"}}
|
||||
{:error, _} -> {:ok, %{success: false, message: "Could not delete maintenance window"}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delete(_parent, _args, _resolution), do: Helpers.authentication_error()
|
||||
|
||||
defp fetch_org_window(id, org_id) do
|
||||
case ScopedResource.fetch(MaintenanceWindow, id, org_id) do
|
||||
{:ok, window} -> {:ok, window}
|
||||
{:error, _} -> {:error, "Maintenance window not found"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,7 @@ defmodule ToweropsWeb.GraphQL.Schema do
|
|||
import_types(ToweropsWeb.GraphQL.Types.Activity)
|
||||
import_types(ToweropsWeb.GraphQL.Types.Schedule)
|
||||
import_types(ToweropsWeb.GraphQL.Types.EscalationPolicy)
|
||||
import_types(ToweropsWeb.GraphQL.Types.MaintenanceWindow)
|
||||
|
||||
query do
|
||||
# Devices
|
||||
|
|
@ -126,6 +127,17 @@ defmodule ToweropsWeb.GraphQL.Schema do
|
|||
arg(:id, non_null(:id))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.get/3)
|
||||
end
|
||||
|
||||
# Maintenance Windows
|
||||
field :maintenance_windows, list_of(:maintenance_window) do
|
||||
arg(:filter, :string)
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.list/3)
|
||||
end
|
||||
|
||||
field :maintenance_window, :maintenance_window do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.get/3)
|
||||
end
|
||||
end
|
||||
|
||||
mutation do
|
||||
|
|
@ -342,5 +354,22 @@ defmodule ToweropsWeb.GraphQL.Schema do
|
|||
arg(:id, non_null(:id))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.EscalationPolicy.delete_target/3)
|
||||
end
|
||||
|
||||
# Maintenance Window CRUD
|
||||
field :create_maintenance_window, :maintenance_window do
|
||||
arg(:input, non_null(:maintenance_window_input))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.create/3)
|
||||
end
|
||||
|
||||
field :update_maintenance_window, :maintenance_window do
|
||||
arg(:id, non_null(:id))
|
||||
arg(:input, non_null(:maintenance_window_input))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.update/3)
|
||||
end
|
||||
|
||||
field :delete_maintenance_window, :delete_result do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&ToweropsWeb.GraphQL.Resolvers.MaintenanceWindow.delete/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
32
lib/towerops_web/graphql/types/maintenance_window.ex
Normal file
32
lib/towerops_web/graphql/types/maintenance_window.ex
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
defmodule ToweropsWeb.GraphQL.Types.MaintenanceWindow do
|
||||
@moduledoc "GraphQL types for maintenance windows."
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
object :maintenance_window do
|
||||
field :id, :id
|
||||
field :name, :string
|
||||
field :reason, :string
|
||||
field :starts_at, :string
|
||||
field :ends_at, :string
|
||||
field :recurring, :boolean
|
||||
field :recurrence_rule, :string
|
||||
field :suppress_alerts, :boolean
|
||||
field :site_id, :id
|
||||
field :device_id, :id
|
||||
field :organization_id, :id
|
||||
field :inserted_at, :string
|
||||
field :updated_at, :string
|
||||
end
|
||||
|
||||
input_object :maintenance_window_input do
|
||||
field :name, :string
|
||||
field :reason, :string
|
||||
field :starts_at, :string
|
||||
field :ends_at, :string
|
||||
field :recurring, :boolean
|
||||
field :recurrence_rule, :string
|
||||
field :suppress_alerts, :boolean
|
||||
field :site_id, :id
|
||||
field :device_id, :id
|
||||
end
|
||||
end
|
||||
|
|
@ -188,6 +188,9 @@ defmodule ToweropsWeb.Router do
|
|||
delete "/escalation_policies/:id/rules/:rule_id", EscalationPoliciesController, :delete_rule
|
||||
post "/escalation_policies/:id/rules/:rule_id/targets", EscalationPoliciesController, :create_target
|
||||
delete "/escalation_policies/:id/rules/:rule_id/targets/:target_id", EscalationPoliciesController, :delete_target
|
||||
|
||||
# Maintenance windows
|
||||
resources "/maintenance_windows", MaintenanceWindowsController, except: [:new, :edit]
|
||||
end
|
||||
|
||||
# Webhook routes (shared secret authentication)
|
||||
|
|
|
|||
22
test/support/fixtures/maintenance_fixtures.ex
Normal file
22
test/support/fixtures/maintenance_fixtures.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Towerops.MaintenanceFixtures do
|
||||
@moduledoc """
|
||||
Test helpers for creating maintenance window entities.
|
||||
"""
|
||||
|
||||
alias Towerops.Maintenance.MaintenanceWindow
|
||||
alias Towerops.Repo
|
||||
|
||||
def maintenance_window_fixture(attrs) do
|
||||
attrs =
|
||||
Enum.into(attrs, %{
|
||||
name: "Maintenance #{System.unique_integer([:positive])}",
|
||||
starts_at: DateTime.add(DateTime.utc_now(), -3600, :second),
|
||||
ends_at: DateTime.add(DateTime.utc_now(), 3600, :second),
|
||||
suppress_alerts: true
|
||||
})
|
||||
|
||||
%MaintenanceWindow{}
|
||||
|> MaintenanceWindow.changeset(attrs)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
defmodule ToweropsWeb.Api.V1.MaintenanceWindowsControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.MaintenanceFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
alias Towerops.Maintenance
|
||||
|
||||
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
|
||||
}
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "lists all maintenance windows for organization", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
_window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows")
|
||||
|
||||
assert %{"maintenance_windows" => windows} = json_response(conn, 200)
|
||||
assert length(windows) == 1
|
||||
|
||||
window_json = List.first(windows)
|
||||
assert Map.has_key?(window_json, "id")
|
||||
assert Map.has_key?(window_json, "name")
|
||||
assert Map.has_key?(window_json, "starts_at")
|
||||
assert Map.has_key?(window_json, "ends_at")
|
||||
end
|
||||
|
||||
test "returns empty list when organization has no windows", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows")
|
||||
|
||||
assert %{"maintenance_windows" => []} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "filters by status", %{conn: conn, user: user, organization: organization} do
|
||||
_active_window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id,
|
||||
starts_at: DateTime.add(DateTime.utc_now(), -3600, :second),
|
||||
ends_at: DateTime.add(DateTime.utc_now(), 3600, :second)
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows?filter=active")
|
||||
|
||||
assert %{"maintenance_windows" => windows} = json_response(conn, 200)
|
||||
assert length(windows) == 1
|
||||
end
|
||||
|
||||
test "does not return windows from other organizations", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
_other_window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: other_org.id,
|
||||
created_by_id: other_user.id
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows")
|
||||
|
||||
assert %{"maintenance_windows" => []} = json_response(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create/2" do
|
||||
test "creates window with valid attributes", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
starts_at = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
|
||||
ends_at = DateTime.utc_now() |> DateTime.add(7200, :second) |> DateTime.to_iso8601()
|
||||
|
||||
params = %{
|
||||
"maintenance_window" => %{
|
||||
"name" => "Scheduled Upgrade",
|
||||
"reason" => "Router firmware upgrade",
|
||||
"starts_at" => starts_at,
|
||||
"ends_at" => ends_at,
|
||||
"suppress_alerts" => true
|
||||
}
|
||||
}
|
||||
|
||||
conn = post(conn, ~p"/api/v1/maintenance_windows", params)
|
||||
|
||||
assert %{
|
||||
"id" => id,
|
||||
"name" => "Scheduled Upgrade",
|
||||
"reason" => "Router firmware upgrade"
|
||||
} = json_response(conn, 201)
|
||||
|
||||
assert is_binary(id)
|
||||
window = Maintenance.get_window!(id)
|
||||
assert window.organization_id == organization.id
|
||||
assert window.created_by_id == user.id
|
||||
end
|
||||
|
||||
test "returns 422 with invalid params", %{conn: conn} do
|
||||
params = %{"maintenance_window" => %{"name" => ""}}
|
||||
|
||||
conn = post(conn, ~p"/api/v1/maintenance_windows", params)
|
||||
|
||||
assert %{"errors" => _} = json_response(conn, 422)
|
||||
end
|
||||
|
||||
test "returns 400 when maintenance_window parameter is missing", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/v1/maintenance_windows", %{})
|
||||
|
||||
assert %{"error" => "Missing 'maintenance_window' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns window when it belongs to organization", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
window =
|
||||
maintenance_window_fixture(%{
|
||||
name: "Test Window",
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows/#{window.id}")
|
||||
|
||||
assert %{"id" => _, "name" => "Test Window"} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 404 when window does not exist", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 403 when window belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
other_window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: other_org.id,
|
||||
created_by_id: other_user.id
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/v1/maintenance_windows/#{other_window.id}")
|
||||
|
||||
assert %{"error" => "Access denied to this maintenance window"} = json_response(conn, 403)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "updates window with valid attributes", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
window =
|
||||
maintenance_window_fixture(%{
|
||||
name: "Original",
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id
|
||||
})
|
||||
|
||||
params = %{"maintenance_window" => %{"name" => "Updated"}}
|
||||
conn = patch(conn, ~p"/api/v1/maintenance_windows/#{window.id}", params)
|
||||
|
||||
assert %{"id" => _, "name" => "Updated"} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 404 when window does not exist", %{conn: conn} do
|
||||
params = %{"maintenance_window" => %{"name" => "Updated"}}
|
||||
conn = patch(conn, ~p"/api/v1/maintenance_windows/#{Ecto.UUID.generate()}", params)
|
||||
|
||||
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 400 when maintenance_window parameter is missing", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id
|
||||
})
|
||||
|
||||
conn = patch(conn, ~p"/api/v1/maintenance_windows/#{window.id}", %{})
|
||||
|
||||
assert %{"error" => "Missing 'maintenance_window' parameter"} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "deletes window successfully", %{conn: conn, user: user, organization: organization} do
|
||||
window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: organization.id,
|
||||
created_by_id: user.id
|
||||
})
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/maintenance_windows/#{window.id}")
|
||||
|
||||
assert %{"success" => true} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 404 when window does not exist", %{conn: conn} do
|
||||
conn = delete(conn, ~p"/api/v1/maintenance_windows/#{Ecto.UUID.generate()}")
|
||||
|
||||
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
|
||||
end
|
||||
|
||||
test "returns 403 when window belongs to different organization", %{conn: conn} do
|
||||
other_user = user_fixture()
|
||||
other_org = organization_fixture(other_user.id)
|
||||
|
||||
other_window =
|
||||
maintenance_window_fixture(%{
|
||||
organization_id: other_org.id,
|
||||
created_by_id: other_user.id
|
||||
})
|
||||
|
||||
conn = delete(conn, ~p"/api/v1/maintenance_windows/#{other_window.id}")
|
||||
|
||||
assert %{"error" => "Access denied to this maintenance window"} = 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/maintenance_windows")
|
||||
|
||||
assert %{"error" => _message} = json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -103,7 +103,7 @@ defmodule ToweropsWeb.AgentLive.EditTest do
|
|||
|
||||
flash = assert_redirect(view, ~p"/agents/#{agent.id}")
|
||||
|
||||
assert flash["info"] =~ "Device updated successfully"
|
||||
assert flash["info"] =~ "Agent updated successfully"
|
||||
end
|
||||
|
||||
test "shows error when saving with blank name", %{conn: conn, organization: org} do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue