398 lines
12 KiB
Elixir
398 lines
12 KiB
Elixir
defmodule ToweropsWeb.Api.V1.SchedulesControllerTest do
|
|
use ToweropsWeb.ConnCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.OnCallFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.ApiTokens
|
|
alias Towerops.OnCall
|
|
|
|
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 schedules for authenticated organization", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
schedule1 = schedule_fixture(organization.id, %{name: "Primary"})
|
|
schedule2 = schedule_fixture(organization.id, %{name: "Secondary"})
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules")
|
|
|
|
assert %{"schedules" => schedules} = json_response(conn, 200)
|
|
assert length(schedules) == 2
|
|
|
|
ids = Enum.map(schedules, & &1["id"])
|
|
assert schedule1.id in ids
|
|
assert schedule2.id in ids
|
|
end
|
|
|
|
test "returns empty list when organization has no schedules", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/schedules")
|
|
|
|
assert %{"schedules" => []} = json_response(conn, 200)
|
|
end
|
|
|
|
test "does not return schedules from other organizations", %{conn: conn} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
_other_schedule = schedule_fixture(other_org.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules")
|
|
|
|
assert %{"schedules" => []} = json_response(conn, 200)
|
|
end
|
|
end
|
|
|
|
describe "create/2" do
|
|
test "creates schedule with valid attributes", %{conn: conn, organization: organization} do
|
|
params = %{
|
|
"schedule" => %{
|
|
"name" => "On-Call Rotation",
|
|
"timezone" => "America/New_York",
|
|
"description" => "Primary rotation"
|
|
}
|
|
}
|
|
|
|
conn = post(conn, ~p"/api/v1/schedules", params)
|
|
|
|
assert %{
|
|
"id" => id,
|
|
"name" => "On-Call Rotation",
|
|
"timezone" => "America/New_York",
|
|
"description" => "Primary rotation"
|
|
} = json_response(conn, 201)
|
|
|
|
assert is_binary(id)
|
|
schedule = OnCall.get_schedule!(id)
|
|
assert schedule.organization_id == organization.id
|
|
end
|
|
|
|
test "returns 422 with invalid params", %{conn: conn} do
|
|
params = %{"schedule" => %{"name" => ""}}
|
|
|
|
conn = post(conn, ~p"/api/v1/schedules", params)
|
|
|
|
assert %{"errors" => errors} = json_response(conn, 422)
|
|
assert Enum.any?(["name", "timezone"], &Map.has_key?(errors, &1))
|
|
end
|
|
|
|
test "returns 400 when schedule parameter is missing", %{conn: conn} do
|
|
conn = post(conn, ~p"/api/v1/schedules", %{})
|
|
|
|
assert %{"error" => "Missing 'schedule' parameter"} = json_response(conn, 400)
|
|
end
|
|
end
|
|
|
|
describe "show/2" do
|
|
test "returns schedule with layers, members, and overrides", %{
|
|
conn: conn,
|
|
user: user,
|
|
organization: organization
|
|
} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id)
|
|
_member = layer_member_fixture(layer.id, user.id)
|
|
_override = override_fixture(schedule.id, user.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules/#{schedule.id}")
|
|
|
|
assert %{
|
|
"id" => _,
|
|
"name" => _,
|
|
"timezone" => _,
|
|
"layers" => [layer_json],
|
|
"overrides" => [override_json]
|
|
} = json_response(conn, 200)
|
|
|
|
assert %{"members" => [_member_json]} = layer_json
|
|
assert Map.has_key?(override_json, "start_time")
|
|
end
|
|
|
|
test "returns 404 when schedule does not exist", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/schedules/#{Ecto.UUID.generate()}")
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
|
|
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
other_schedule = schedule_fixture(other_org.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules/#{other_schedule.id}")
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "update/2" do
|
|
test "updates schedule with valid attributes", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id, %{name: "Original"})
|
|
|
|
params = %{"schedule" => %{"name" => "Updated"}}
|
|
conn = patch(conn, ~p"/api/v1/schedules/#{schedule.id}", params)
|
|
|
|
assert %{"id" => _, "name" => "Updated"} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns 404 when schedule does not exist", %{conn: conn} do
|
|
params = %{"schedule" => %{"name" => "Updated"}}
|
|
conn = patch(conn, ~p"/api/v1/schedules/#{Ecto.UUID.generate()}", params)
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
|
|
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
other_schedule = schedule_fixture(other_org.id)
|
|
|
|
params = %{"schedule" => %{"name" => "Hacked"}}
|
|
conn = patch(conn, ~p"/api/v1/schedules/#{other_schedule.id}", params)
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
|
|
test "returns 400 when schedule parameter is missing", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
conn = patch(conn, ~p"/api/v1/schedules/#{schedule.id}", %{})
|
|
|
|
assert %{"error" => "Missing 'schedule' parameter"} = json_response(conn, 400)
|
|
end
|
|
end
|
|
|
|
describe "delete/2" do
|
|
test "deletes schedule successfully", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
conn = delete(conn, ~p"/api/v1/schedules/#{schedule.id}")
|
|
|
|
assert %{"success" => true} = json_response(conn, 200)
|
|
assert OnCall.get_schedule(schedule.id) == nil
|
|
end
|
|
|
|
test "returns 404 when schedule does not exist", %{conn: conn} do
|
|
conn = delete(conn, ~p"/api/v1/schedules/#{Ecto.UUID.generate()}")
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
|
|
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
other_schedule = schedule_fixture(other_org.id)
|
|
|
|
conn = delete(conn, ~p"/api/v1/schedules/#{other_schedule.id}")
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "on_call/2" do
|
|
test "returns on-call user for schedule", %{
|
|
conn: conn,
|
|
user: user,
|
|
organization: organization
|
|
} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id, %{start_date: ~U[2025-01-01 09:00:00Z]})
|
|
_member = layer_member_fixture(layer.id, user.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules/#{schedule.id}/on_call")
|
|
|
|
assert %{"on_call" => on_call} = json_response(conn, 200)
|
|
assert on_call["id"] == user.id
|
|
end
|
|
|
|
test "returns null when no one is on call", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
conn = get(conn, ~p"/api/v1/schedules/#{schedule.id}/on_call")
|
|
|
|
assert %{"on_call" => nil} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns 404 when schedule does not exist", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/schedules/#{Ecto.UUID.generate()}/on_call")
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "create_layer/2" do
|
|
test "creates layer on schedule", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
params = %{
|
|
"layer" => %{
|
|
"name" => "Primary",
|
|
"position" => 1,
|
|
"rotation_type" => "weekly",
|
|
"rotation_interval" => 1,
|
|
"handoff_time" => "09:00:00",
|
|
"start_date" => "2026-01-01T09:00:00Z"
|
|
}
|
|
}
|
|
|
|
conn = post(conn, ~p"/api/v1/schedules/#{schedule.id}/layers", params)
|
|
|
|
assert %{"id" => _, "name" => "Primary", "rotation_type" => "weekly"} =
|
|
json_response(conn, 201)
|
|
end
|
|
|
|
test "returns 404 when schedule does not exist", %{conn: conn} do
|
|
params = %{
|
|
"layer" => %{
|
|
"name" => "Primary",
|
|
"position" => 1,
|
|
"rotation_type" => "weekly",
|
|
"rotation_interval" => 1,
|
|
"handoff_time" => "09:00:00",
|
|
"start_date" => "2026-01-01T09:00:00Z"
|
|
}
|
|
}
|
|
|
|
conn = post(conn, ~p"/api/v1/schedules/#{Ecto.UUID.generate()}/layers", params)
|
|
|
|
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "update_layer/2" do
|
|
test "updates layer", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id)
|
|
|
|
params = %{"layer" => %{"name" => "Updated Layer"}}
|
|
conn = patch(conn, ~p"/api/v1/schedules/#{schedule.id}/layers/#{layer.id}", params)
|
|
|
|
assert %{"name" => "Updated Layer"} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns 404 when layer does not exist", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
params = %{"layer" => %{"name" => "Updated"}}
|
|
|
|
conn =
|
|
patch(conn, ~p"/api/v1/schedules/#{schedule.id}/layers/#{Ecto.UUID.generate()}", params)
|
|
|
|
assert %{"error" => "Layer not found"} = json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "delete_layer/2" do
|
|
test "deletes layer", %{conn: conn, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id)
|
|
|
|
conn = delete(conn, ~p"/api/v1/schedules/#{schedule.id}/layers/#{layer.id}")
|
|
|
|
assert %{"success" => true} = json_response(conn, 200)
|
|
end
|
|
end
|
|
|
|
describe "create_member/2" do
|
|
test "adds member to layer", %{conn: conn, user: user, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id)
|
|
|
|
params = %{
|
|
"member" => %{
|
|
"user_id" => user.id,
|
|
"position" => 0
|
|
}
|
|
}
|
|
|
|
conn =
|
|
post(conn, ~p"/api/v1/schedules/#{schedule.id}/layers/#{layer.id}/members", params)
|
|
|
|
assert %{"id" => _, "user_id" => uid, "position" => 0} = json_response(conn, 201)
|
|
assert uid == user.id
|
|
end
|
|
end
|
|
|
|
describe "delete_member/2" do
|
|
test "removes member from layer", %{conn: conn, user: user, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
layer = layer_fixture(schedule.id)
|
|
member = layer_member_fixture(layer.id, user.id)
|
|
|
|
conn =
|
|
delete(
|
|
conn,
|
|
~p"/api/v1/schedules/#{schedule.id}/layers/#{layer.id}/members/#{member.id}"
|
|
)
|
|
|
|
assert %{"success" => true} = json_response(conn, 200)
|
|
end
|
|
end
|
|
|
|
describe "create_override/2" do
|
|
test "creates override on schedule", %{conn: conn, user: user, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
|
|
params = %{
|
|
"override" => %{
|
|
"user_id" => user.id,
|
|
"start_time" => "2026-03-15T09:00:00Z",
|
|
"end_time" => "2026-03-16T09:00:00Z"
|
|
}
|
|
}
|
|
|
|
conn = post(conn, ~p"/api/v1/schedules/#{schedule.id}/overrides", params)
|
|
|
|
assert %{"id" => _, "start_time" => _, "end_time" => _} = json_response(conn, 201)
|
|
end
|
|
end
|
|
|
|
describe "delete_override/2" do
|
|
test "deletes override", %{conn: conn, user: user, organization: organization} do
|
|
schedule = schedule_fixture(organization.id)
|
|
override = override_fixture(schedule.id, user.id)
|
|
|
|
conn =
|
|
delete(conn, ~p"/api/v1/schedules/#{schedule.id}/overrides/#{override.id}")
|
|
|
|
assert %{"success" => true} = json_response(conn, 200)
|
|
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/schedules")
|
|
|
|
assert %{"error" => _message} = json_response(conn, 401)
|
|
end
|
|
end
|
|
end
|