diff --git a/CHANGELOG.md b/CHANGELOG.md index efad1eee386..a16735babf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ and this project adheres to is ambiguous across projects, the API returns 409 with guidance to add `?project_id=`. Existing unscoped calls keep working for unambiguous names. [#3548](https://github.com/OpenFn/lightning/issues/3548) +- Ability to filter work orders and runs via REST API by UUIDs or status; added + example curl requests to REST API docs. + [#4552](https://github.com/OpenFn/lightning/issues/4552) ### Changed diff --git a/lib/lightning/invocation/query.ex b/lib/lightning/invocation/query.ex index 40a34cb60ea..42cae1d5cc3 100644 --- a/lib/lightning/invocation/query.ex +++ b/lib/lightning/invocation/query.ex @@ -122,6 +122,7 @@ defmodule Lightning.Invocation.Query do |> filter_runs_by_project(params["project_id"]) |> filter_runs_by_workflow(params["workflow_id"]) |> filter_runs_by_work_order(params["work_order_id"]) + |> filter_runs_by_state(params["state"]) end defp filter_runs_by_project(query, nil), do: query @@ -142,6 +143,43 @@ defmodule Lightning.Invocation.Query do from([work_order: wo] in query, where: wo.id == ^work_order_id) end + @valid_run_states Lightning.Run.states() |> Enum.map(&to_string/1) + + defp filter_runs_by_state(query, nil), do: query + + defp filter_runs_by_state(query, state) when is_binary(state) do + states = + state + |> String.split(",", trim: true) + |> Enum.map(&String.to_existing_atom/1) + + from(r in query, where: r.state in ^states) + end + + @doc """ + Validates the `state` query parameter against known run states. + + Returns `:ok` or `{:error, message}`. + """ + @spec validate_run_state_param(map()) :: :ok | {:error, String.t()} + def validate_run_state_param(%{"state" => state}) when is_binary(state) do + invalid = + state + |> String.split(",", trim: true) + |> Enum.reject(&(&1 in @valid_run_states)) + + case invalid do + [] -> + :ok + + bad -> + {:error, + "Invalid state filter: #{inspect(bad)}. Valid states are: #{Enum.join(@valid_run_states, ", ")}"} + end + end + + def validate_run_state_param(_params), do: :ok + defp filter_by_inserted_after(query, nil), do: query defp filter_by_inserted_after(query, date_string) do @@ -304,9 +342,27 @@ defmodule Lightning.Invocation.Query do @spec filter_work_orders(Ecto.Queryable.t(), map()) :: Ecto.Queryable.t() def filter_work_orders(query, params) do query + |> filter_work_orders_by_ids(params["id"]) |> filter_work_orders_by_date(params) |> filter_work_orders_by_project(params["project_id"]) |> filter_work_orders_by_workflow(params["workflow_id"]) + |> filter_work_orders_by_state(params["state"]) + end + + defp filter_work_orders_by_ids(query, nil), do: query + + defp filter_work_orders_by_ids(query, ids) when is_list(ids) do + from(wo in query, where: wo.id in ^ids) + end + + defp filter_work_orders_by_ids(query, id) when is_binary(id) do + case String.split(id, ",", trim: true) do + [single_id] -> + from(wo in query, where: wo.id == ^single_id) + + ids -> + from(wo in query, where: wo.id in ^ids) + end end defp filter_work_orders_by_project(query, nil), do: query @@ -321,6 +377,43 @@ defmodule Lightning.Invocation.Query do from([workflow: w] in query, where: w.id == ^workflow_id) end + @valid_states Lightning.WorkOrder.states() |> Enum.map(&to_string/1) + + defp filter_work_orders_by_state(query, nil), do: query + + defp filter_work_orders_by_state(query, state) when is_binary(state) do + states = + state + |> String.split(",", trim: true) + |> Enum.map(&String.to_existing_atom/1) + + from(wo in query, where: wo.state in ^states) + end + + @doc """ + Validates the `state` query parameter against known work order states. + + Returns `:ok` or `{:error, message}`. + """ + @spec validate_state_param(map()) :: :ok | {:error, String.t()} + def validate_state_param(%{"state" => state}) when is_binary(state) do + invalid = + state + |> String.split(",", trim: true) + |> Enum.reject(&(&1 in @valid_states)) + + case invalid do + [] -> + :ok + + bad -> + {:error, + "Invalid state filter: #{inspect(bad)}. Valid states are: #{Enum.join(@valid_states, ", ")}"} + end + end + + def validate_state_param(_params), do: :ok + defp filter_wo_by_inserted_after(query, nil), do: query defp filter_wo_by_inserted_after(query, date_string) do diff --git a/lib/lightning/jobs.ex b/lib/lightning/jobs.ex index 55a66da5e56..954799e10a1 100644 --- a/lib/lightning/jobs.ex +++ b/lib/lightning/jobs.ex @@ -122,9 +122,16 @@ defmodule Lightning.Jobs do Gets a single job. Returns `{:ok, job}` if found, `{:error, :not_found}` otherwise. + + ## Options + + * `:include` - list of associations to preload (default: `[]`) + """ - def get_job(id) do - case Repo.get(Job, id) |> Repo.preload([:workflow]) do + def get_job(id, opts \\ []) do + preloads = Keyword.get(opts, :include, []) + + case Repo.get(Job, id) |> Repo.preload(preloads) do nil -> {:error, :not_found} job -> {:ok, job} end diff --git a/lib/lightning/runs/run.ex b/lib/lightning/runs/run.ex index 58dadf94510..c5bb69a27c6 100644 --- a/lib/lightning/runs/run.ex +++ b/lib/lightning/runs/run.ex @@ -51,6 +51,13 @@ defmodule Lightning.Run do :lost ] + @states [:available, :claimed, :started] ++ @final_states + + @doc """ + Returns all possible states for a run. + """ + def states, do: @states + @doc """ Returns the list of final states for a run. """ @@ -98,15 +105,7 @@ defmodule Lightning.Run do embeds_one :options, Lightning.Runs.RunOptions field :state, Ecto.Enum, - values: - Enum.concat( - [ - :available, - :claimed, - :started - ], - @final_states - ), + values: @states, default: :available field :error_type, :string diff --git a/lib/lightning/workorders/workorder.ex b/lib/lightning/workorders/workorder.ex index b95e03ecb04..0802f71f00d 100644 --- a/lib/lightning/workorders/workorder.ex +++ b/lib/lightning/workorders/workorder.ex @@ -26,6 +26,11 @@ defmodule Lightning.WorkOrder do Run.final_states() ) + @doc """ + Returns all possible states for a work order. + """ + def states, do: @state_values + @derive {Jason.Encoder, only: [ :id, diff --git a/lib/lightning_web/controllers/api/ai_assistant_controller.ex b/lib/lightning_web/controllers/api/ai_assistant_controller.ex index b6bd6ad1309..c35e12cb7b6 100644 --- a/lib/lightning_web/controllers/api/ai_assistant_controller.ex +++ b/lib/lightning_web/controllers/api/ai_assistant_controller.ex @@ -98,7 +98,9 @@ defmodule LightningWeb.API.AiAssistantController do alias Lightning.Repo import Ecto.Query - case Jobs.get_job(job_id) do + case Jobs.get_job(job_id, + include: [workflow: [project: :project_users]] + ) do {:ok, job} -> check_job_access(job, user) @@ -125,13 +127,7 @@ defmodule LightningWeb.API.AiAssistantController do end defp check_job_access(job, user) do - alias Lightning.Repo - - workflow = - job.workflow - |> Repo.preload(project: [:project_users]) - - check_workflow_access(workflow, user) + check_workflow_access(job.workflow, user) end defp check_unsaved_job_access(nil, _user) do diff --git a/lib/lightning_web/controllers/api/job_controller.ex b/lib/lightning_web/controllers/api/job_controller.ex index a417753ecc3..6378467db60 100644 --- a/lib/lightning_web/controllers/api/job_controller.ex +++ b/lib/lightning_web/controllers/api/job_controller.ex @@ -17,6 +17,36 @@ defmodule LightningWeb.API.JobController do GET /api/jobs GET /api/jobs?project_id=a1b2c3d4-...&page=1&page_size=20 GET /api/jobs/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d + + ## Sample curl requests + + List all jobs: + + ```bash + curl http://localhost:4000/api/jobs \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Get a single job: + + ```bash + curl http://localhost:4000/api/jobs/$JOB_ID \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Filter by project: + + ```bash + curl "http://localhost:4000/api/jobs?project_id=$PROJECT_ID" \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Nested route — jobs for a specific project: + + ```bash + curl http://localhost:4000/api/projects/$PROJECT_ID/jobs \\ + -H "Authorization: Bearer $TOKEN" + ``` """ use LightningWeb, :controller @@ -115,14 +145,13 @@ defmodule LightningWeb.API.JobController do """ @spec show(Plug.Conn.t(), map()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do - with job <- Jobs.get_job!(id), - job_with_project <- Lightning.Repo.preload(job, workflow: :project), + with {:ok, job} <- Jobs.get_job(id, include: [workflow: :project]), :ok <- ProjectUsers |> Permissions.can( :access_project, conn.assigns.current_resource, - job_with_project.workflow.project + job.workflow.project ) do render(conn, "show.json", job: job, conn: conn) end diff --git a/lib/lightning_web/controllers/api/project_controller.ex b/lib/lightning_web/controllers/api/project_controller.ex index 888d20f7b45..9b0edba3774 100644 --- a/lib/lightning_web/controllers/api/project_controller.ex +++ b/lib/lightning_web/controllers/api/project_controller.ex @@ -14,6 +14,22 @@ defmodule LightningWeb.API.ProjectController do GET /api/projects?page=1&page_size=20 GET /api/projects/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d + + ## Sample curl requests + + List all projects: + + ```bash + curl http://localhost:4000/api/projects \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Get a single project: + + ```bash + curl http://localhost:4000/api/projects/$PROJECT_ID \\ + -H "Authorization: Bearer $TOKEN" + ``` """ use LightningWeb, :controller @@ -78,7 +94,8 @@ defmodule LightningWeb.API.ProjectController do """ @spec show(Plug.Conn.t(), map()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do - with project <- Projects.get_project(id), + with %Lightning.Projects.Project{} = project <- + Projects.get_project(id), :ok <- ProjectUsers |> Permissions.can( @@ -87,6 +104,9 @@ defmodule LightningWeb.API.ProjectController do project ) do render(conn, "show.json", project: project, conn: conn) + else + nil -> {:error, :not_found} + error -> error end end end diff --git a/lib/lightning_web/controllers/api/run_controller.ex b/lib/lightning_web/controllers/api/run_controller.ex index b04ef94a608..7fb4346cf64 100644 --- a/lib/lightning_web/controllers/api/run_controller.ex +++ b/lib/lightning_web/controllers/api/run_controller.ex @@ -6,6 +6,7 @@ defmodule LightningWeb.API.RunController do - `page` - Page number (default: 1) - `page_size` - Number of items per page (default: 10) + - `state` - Filter by state (comma-separated). Valid values: available, claimed, started, success, failed, crashed, cancelled, killed, exception, lost - `inserted_after` - Filter runs created after this ISO8601 datetime - `inserted_before` - Filter runs created before this ISO8601 datetime - `updated_after` - Filter runs updated after this ISO8601 datetime @@ -18,6 +19,36 @@ defmodule LightningWeb.API.RunController do GET /api/runs?inserted_after=2024-01-01T00:00:00Z&inserted_before=2024-12-31T23:59:59Z GET /api/projects/:project_id/runs?inserted_after=2024-01-01T00:00:00Z + ## Sample curl requests + + List all runs: + + ```bash + curl http://localhost:4000/api/runs \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Get a single run: + + ```bash + curl http://localhost:4000/api/runs/$RUN_ID \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Filter by project, workflow, work order, or date range: + + ```bash + curl "http://localhost:4000/api/runs?project_id=$PID&inserted_after=2024-01-01T00:00:00Z" \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Nested route — runs for a specific project: + + ```bash + curl http://localhost:4000/api/projects/$PROJECT_ID/runs \\ + -H "Authorization: Bearer $TOKEN" + ``` + """ use LightningWeb, :controller @@ -44,6 +75,7 @@ defmodule LightningWeb.API.RunController do - `project_id` - Project UUID (optional, filters to specific project) - `page` - Page number (optional, default: 1) - `page_size` - Items per page (optional, default: 10) + - `state` - Comma-separated list of states to filter by (optional) - `inserted_after` - Filter runs created after ISO8601 datetime (optional) - `inserted_before` - Filter runs created before ISO8601 datetime (optional) - `updated_after` - Filter runs updated after ISO8601 datetime (optional) @@ -79,6 +111,7 @@ defmodule LightningWeb.API.RunController do "updated_after", "updated_before" ]), + :ok <- Invocation.Query.validate_run_state_param(params), project <- Lightning.Projects.get_project(project_id), :ok <- ProjectUsers @@ -103,7 +136,8 @@ defmodule LightningWeb.API.RunController do "inserted_before", "updated_after", "updated_before" - ]) do + ]), + :ok <- Invocation.Query.validate_run_state_param(params) do pagination_attrs = Map.take(params, ["page_size", "page"]) page = @@ -140,7 +174,8 @@ defmodule LightningWeb.API.RunController do """ @spec show(Plug.Conn.t(), map()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do - with run <- Runs.get(id, include: [work_order: [workflow: :project]]), + with %Lightning.Run{} = run <- + Runs.get(id, include: [work_order: [workflow: :project]]), :ok <- ProjectUsers |> Permissions.can( @@ -149,6 +184,9 @@ defmodule LightningWeb.API.RunController do run.work_order.workflow.project ) do render(conn, "show.json", run: run, conn: conn) + else + nil -> {:error, :not_found} + error -> error end end end diff --git a/lib/lightning_web/controllers/api/work_orders_controller.ex b/lib/lightning_web/controllers/api/work_orders_controller.ex index 2d3ea6fb89e..09ef28d042d 100644 --- a/lib/lightning_web/controllers/api/work_orders_controller.ex +++ b/lib/lightning_web/controllers/api/work_orders_controller.ex @@ -6,6 +6,7 @@ defmodule LightningWeb.API.WorkOrdersController do - `page` - Page number (default: 1) - `page_size` - Number of items per page (default: 10) + - `state` - Filter by state (comma-separated). Valid values: rejected, pending, running, success, failed, crashed, cancelled, killed, exception, lost - `inserted_after` - Filter work orders created after this ISO8601 datetime - `inserted_before` - Filter work orders created before this ISO8601 datetime - `updated_after` - Filter work orders updated after this ISO8601 datetime @@ -18,6 +19,43 @@ defmodule LightningWeb.API.WorkOrdersController do GET /api/work_orders?inserted_after=2024-01-01T00:00:00Z&inserted_before=2024-12-31T23:59:59Z GET /api/projects/:project_id/work_orders?inserted_after=2024-01-01T00:00:00Z + ## Sample curl requests + + List all work orders: + + ```bash + curl http://localhost:4000/api/work_orders \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Get a single work order: + + ```bash + curl http://localhost:4000/api/work_orders/$WORK_ORDER_ID \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Filter by comma-separated IDs: + + ```bash + curl "http://localhost:4000/api/work_orders?id=$ID1,$ID2" \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Filter by project, workflow, or date range: + + ```bash + curl "http://localhost:4000/api/work_orders?project_id=$PID&inserted_after=2024-01-01T00:00:00Z" \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Nested route — work orders for a specific project: + + ```bash + curl http://localhost:4000/api/projects/$PROJECT_ID/work_orders \\ + -H "Authorization: Bearer $TOKEN" + ``` + """ use LightningWeb, :controller @@ -44,6 +82,7 @@ defmodule LightningWeb.API.WorkOrdersController do - `project_id` - Project UUID (optional, filters to specific project) - `page` - Page number (optional, default: 1) - `page_size` - Items per page (optional, default: 10) + - `state` - Comma-separated list of states to filter by (optional) - `inserted_after` - Filter work orders created after ISO8601 datetime (optional) - `inserted_before` - Filter work orders created before ISO8601 datetime (optional) - `updated_after` - Filter work orders updated after ISO8601 datetime (optional) @@ -79,6 +118,7 @@ defmodule LightningWeb.API.WorkOrdersController do "updated_after", "updated_before" ]), + :ok <- Invocation.Query.validate_state_param(params), project <- Lightning.Projects.get_project(project_id), :ok <- ProjectUsers @@ -103,7 +143,8 @@ defmodule LightningWeb.API.WorkOrdersController do "inserted_before", "updated_after", "updated_before" - ]) do + ]), + :ok <- Invocation.Query.validate_state_param(params) do pagination_attrs = Map.take(params, ["page_size", "page"]) page = @@ -140,7 +181,7 @@ defmodule LightningWeb.API.WorkOrdersController do """ @spec show(Plug.Conn.t(), map()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do - with work_order <- + with %Lightning.WorkOrder{} = work_order <- WorkOrders.get(id, include: [workflow: :project, runs: []]), :ok <- ProjectUsers @@ -150,6 +191,9 @@ defmodule LightningWeb.API.WorkOrdersController do work_order.workflow.project ) do render(conn, "show.json", work_order: work_order, conn: conn) + else + nil -> {:error, :not_found} + error -> error end end end diff --git a/lib/lightning_web/controllers/api/workflows_controller.ex b/lib/lightning_web/controllers/api/workflows_controller.ex index 4ff62b7b4bb..f42bca7b9d9 100644 --- a/lib/lightning_web/controllers/api/workflows_controller.ex +++ b/lib/lightning_web/controllers/api/workflows_controller.ex @@ -27,6 +27,54 @@ defmodule LightningWeb.API.WorkflowsController do GET /api/workflows/a1b2c3d4-... POST /api/workflows PATCH /api/workflows/a1b2c3d4-... + + ## Sample curl requests + + List all workflows: + + ```bash + curl http://localhost:4000/api/workflows \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Get a single workflow: + + ```bash + curl http://localhost:4000/api/workflows/$WORKFLOW_ID \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Filter by project: + + ```bash + curl "http://localhost:4000/api/workflows?project_id=$PROJECT_ID" \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Nested route — workflows for a specific project: + + ```bash + curl http://localhost:4000/api/projects/$PROJECT_ID/workflows \\ + -H "Authorization: Bearer $TOKEN" + ``` + + Create a workflow: + + ```bash + curl -X POST http://localhost:4000/api/projects/$PROJECT_ID/workflows \\ + -H "Authorization: Bearer $TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{"name":"My Workflow","jobs":[...],"triggers":[...],"edges":[...]}' + ``` + + Update a workflow: + + ```bash + curl -X PATCH http://localhost:4000/api/projects/$PROJECT_ID/workflows/$WORKFLOW_ID \\ + -H "Authorization: Bearer $TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{"name":"Updated Name"}' + ``` """ use LightningWeb, :controller diff --git a/test/lightning_web/controllers/api/credential_controller_test.exs b/test/lightning_web/controllers/api/credential_controller_test.exs index d2b41c5aed6..ad100c71c33 100644 --- a/test/lightning_web/controllers/api/credential_controller_test.exs +++ b/test/lightning_web/controllers/api/credential_controller_test.exs @@ -3,6 +3,25 @@ defmodule LightningWeb.API.CredentialControllerTest do import Lightning.Factories + # Sample curl requests: + # + # # List your credentials + # curl http://localhost:4000/api/credentials \ + # -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" + # + # # List credentials for a project + # curl http://localhost:4000/api/projects/$PROJECT_ID/credentials \ + # -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" + # + # # Create a credential + # curl -X POST http://localhost:4000/api/credentials \ + # -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + # -d '{"name":"My Cred","schema":"raw","credential_bodies":[{"name":"main","body":{"key":"val"}}]}' + # + # # Delete a credential + # curl -X DELETE http://localhost:4000/api/credentials/$CREDENTIAL_ID \ + # -H "Authorization: Bearer $TOKEN" + setup %{conn: conn} do {:ok, conn: put_req_header(conn, "accept", "application/json")} end diff --git a/test/lightning_web/controllers/api/job_controller_test.exs b/test/lightning_web/controllers/api/job_controller_test.exs index 50ae7e60a26..e1655b01eb6 100644 --- a/test/lightning_web/controllers/api/job_controller_test.exs +++ b/test/lightning_web/controllers/api/job_controller_test.exs @@ -75,6 +75,11 @@ defmodule LightningWeb.API.JobControllerTest do describe "show" do setup [:assign_bearer_for_api, :create_project_for_current_user, :create_job] + test "returns 404 for non-existent job", %{conn: conn} do + conn = get(conn, ~p"/api/jobs/#{Ecto.UUID.generate()}") + assert json_response(conn, 404) + end + test "shows the job", %{conn: conn, job: job} do conn = get(conn, ~p"/api/jobs/#{job}") response = json_response(conn, 200) diff --git a/test/lightning_web/controllers/api/project_controller_test.exs b/test/lightning_web/controllers/api/project_controller_test.exs index a38394797df..f4e10d2b650 100644 --- a/test/lightning_web/controllers/api/project_controller_test.exs +++ b/test/lightning_web/controllers/api/project_controller_test.exs @@ -80,6 +80,11 @@ defmodule LightningWeb.API.ProjectControllerTest do describe "show" do setup [:assign_bearer_for_api, :create_project_for_current_user] + test "returns 404 for non-existent project", %{conn: conn} do + conn = get(conn, ~p"/api/projects/#{Ecto.UUID.generate()}") + assert json_response(conn, 404) + end + test "with token for other project", %{conn: conn} do other_project = insert(:project) conn = get(conn, ~p"/api/projects/#{other_project.id}") diff --git a/test/lightning_web/controllers/api/run_controller_test.exs b/test/lightning_web/controllers/api/run_controller_test.exs index 2beb038b451..3d17b1ad7fc 100644 --- a/test/lightning_web/controllers/api/run_controller_test.exs +++ b/test/lightning_web/controllers/api/run_controller_test.exs @@ -473,6 +473,142 @@ defmodule LightningWeb.API.RunControllerTest do refute Enum.any?(response["data"], fn r -> r["id"] == run2.id end) end + test "filters runs by state", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + started_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :started, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + _success_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :success, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + started_run = List.first(started_wo.runs) + + conn = get(conn, ~p"/api/runs?state=started") + + response = json_response(conn, 200) + + assert length(response["data"]) == 1 + assert List.first(response["data"])["id"] == started_run.id + end + + test "filters runs by multiple comma-separated states", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + started_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :started, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + failed_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :failed, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + _success_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :success, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + started_run = List.first(started_wo.runs) + failed_run = List.first(failed_wo.runs) + + conn = get(conn, ~p"/api/runs?state=started,failed") + + response = json_response(conn, 200) + + assert length(response["data"]) == 2 + ids = Enum.map(response["data"], & &1["id"]) + assert started_run.id in ids + assert failed_run.id in ids + end + + test "returns error for invalid state filter", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :started, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + conn = get(conn, ~p"/api/runs?state=bogus") + + response = json_response(conn, 400) + + assert %{"error" => error_message} = response + assert error_message =~ "Invalid state filter" + assert error_message =~ "bogus" + end + test "nested route: lists runs for specific project", %{ conn: conn, user: user @@ -688,4 +824,73 @@ defmodule LightningWeb.API.RunControllerTest do assert error_message =~ "123456" end end + + describe "show" do + setup [:assign_bearer_for_api, :create_project_for_current_user] + + test "returns a single run by id", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + workorder = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :started, + dataclip: build(:dataclip), + starting_trigger: trigger + } + ] + ) + + run = List.first(workorder.runs) + + conn = get(conn, ~p"/api/runs/#{run.id}") + + response = json_response(conn, 200) + + assert response["data"]["id"] == run.id + assert response["data"]["type"] == "runs" + assert Map.has_key?(response["data"]["attributes"], "state") + assert Map.has_key?(response["data"]["attributes"], "started_at") + assert Map.has_key?(response["data"]["attributes"], "finished_at") + end + + test "returns 404 for non-existent run", %{conn: conn} do + conn = get(conn, ~p"/api/runs/#{Ecto.UUID.generate()}") + assert json_response(conn, 404) + end + + test "returns 401 for run in project user cannot access", %{conn: conn} do + other_project = insert(:project) + other_workflow = insert(:workflow, project: other_project) + other_trigger = insert(:trigger, workflow: other_workflow) + + other_workorder = + insert(:workorder, + workflow: other_workflow, + trigger: other_trigger, + dataclip: build(:dataclip), + runs: [ + %{ + state: :started, + dataclip: build(:dataclip), + starting_trigger: other_trigger + } + ] + ) + + other_run = List.first(other_workorder.runs) + + conn = get(conn, ~p"/api/runs/#{other_run.id}") + + assert json_response(conn, 401) + end + end end diff --git a/test/lightning_web/controllers/api/work_orders_controller_test.exs b/test/lightning_web/controllers/api/work_orders_controller_test.exs index b9886e03165..453bd843400 100644 --- a/test/lightning_web/controllers/api/work_orders_controller_test.exs +++ b/test/lightning_web/controllers/api/work_orders_controller_test.exs @@ -445,5 +445,267 @@ defmodule LightningWeb.API.WorkOrdersControllerTest do assert error_message =~ "Invalid datetime format for 'inserted_after'" assert error_message =~ "123456" end + + test "filters work orders by state", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + pending_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + state: :pending + ) + + _failed_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + state: :failed + ) + + conn = get(conn, ~p"/api/work_orders?state=pending") + + response = json_response(conn, 200) + + assert length(response["data"]) == 1 + assert List.first(response["data"])["id"] == pending_wo.id + end + + test "filters work orders by multiple comma-separated states", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + pending_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + state: :pending + ) + + failed_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + state: :failed + ) + + _success_wo = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip), + state: :success + ) + + conn = get(conn, ~p"/api/work_orders?state=pending,failed") + + response = json_response(conn, 200) + + assert length(response["data"]) == 2 + ids = Enum.map(response["data"], & &1["id"]) + assert pending_wo.id in ids + assert failed_wo.id in ids + end + + test "returns error for invalid state filter", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + conn = get(conn, ~p"/api/work_orders?state=bogus") + + response = json_response(conn, 400) + + assert %{"error" => error_message} = response + assert error_message =~ "Invalid state filter" + assert error_message =~ "bogus" + end + + test "filters work orders by a single id", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + workorder1 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + _workorder2 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + conn = get(conn, ~p"/api/work_orders?id=#{workorder1.id}") + + response = json_response(conn, 200) + + assert length(response["data"]) == 1 + assert List.first(response["data"])["id"] == workorder1.id + end + + test "filters work orders by an array of ids", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + workorder1 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + workorder2 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + _workorder3 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + conn = + get( + conn, + "/api/work_orders?id[]=#{workorder1.id}&id[]=#{workorder2.id}" + ) + + response = json_response(conn, 200) + + assert length(response["data"]) == 2 + ids = Enum.map(response["data"], & &1["id"]) + assert workorder1.id in ids + assert workorder2.id in ids + end + + test "filters work orders by comma-separated ids", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + workorder1 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + workorder2 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + _workorder3 = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + conn = + get( + conn, + "/api/work_orders?id=#{workorder1.id},#{workorder2.id}" + ) + + response = json_response(conn, 200) + + assert length(response["data"]) == 2 + ids = Enum.map(response["data"], & &1["id"]) + assert workorder1.id in ids + assert workorder2.id in ids + end + end + + describe "show" do + setup [:assign_bearer_for_api, :create_project_for_current_user] + + test "returns a single work order by id", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project) + trigger = insert(:trigger, workflow: workflow) + + workorder = + insert(:workorder, + workflow: workflow, + trigger: trigger, + dataclip: build(:dataclip) + ) + + conn = get(conn, ~p"/api/work_orders/#{workorder}") + + response = json_response(conn, 200) + + assert response["data"]["id"] == workorder.id + assert response["data"]["type"] == "work_orders" + assert Map.has_key?(response["data"]["attributes"], "state") + assert Map.has_key?(response["data"]["attributes"], "last_activity") + assert Map.has_key?(response["data"]["attributes"], "inserted_at") + assert Map.has_key?(response["data"]["attributes"], "updated_at") + end + + test "returns 404 for non-existent work order", %{conn: conn} do + conn = get(conn, ~p"/api/work_orders/#{Ecto.UUID.generate()}") + + assert json_response(conn, 404) + end + + test "returns 401 for work order in project user cannot access", %{ + conn: conn + } do + other_project = insert(:project) + other_workflow = insert(:workflow, project: other_project) + other_trigger = insert(:trigger, workflow: other_workflow) + + other_workorder = + insert(:workorder, + workflow: other_workflow, + trigger: other_trigger, + dataclip: build(:dataclip) + ) + + conn = get(conn, ~p"/api/work_orders/#{other_workorder}") + + assert json_response(conn, 401) + end end end diff --git a/test/lightning_web/workflows_controller_test.exs b/test/lightning_web/controllers/api/workflows_controller_test.exs similarity index 75% rename from test/lightning_web/workflows_controller_test.exs rename to test/lightning_web/controllers/api/workflows_controller_test.exs index 4f7317a9850..fcd15ca5c4f 100644 --- a/test/lightning_web/workflows_controller_test.exs +++ b/test/lightning_web/controllers/api/workflows_controller_test.exs @@ -18,43 +18,162 @@ defmodule LightningWeb.API.WorkflowsControllerTest do {:ok, conn: conn} end - describe "GET /workflows" do - test "returns a list of workflows", %{conn: conn} do - user = insert(:user) + describe "index" do + setup [:assign_bearer_for_api, :create_project_for_current_user] - project = - insert(:project, project_users: [%{user: user}]) + test "lists workflows for projects I have access to", %{ + conn: conn, + project: project + } do + workflow1 = insert(:workflow, project: project, name: "Workflow A") + insert(:trigger, workflow: workflow1) + insert(:job, workflow: workflow1) - workflow1 = insert(:simple_workflow, name: "workf-A", project: project) - workflow2 = insert(:simple_workflow, name: "workf-B", project: project) - _workflow = insert(:simple_workflow) + workflow2 = insert(:workflow, project: project, name: "Workflow B") + insert(:trigger, workflow: workflow2) - conn = - conn - |> assign_bearer(user) - |> get(~p"/api/projects/#{project.id}/workflows/") + # Create a workflow in another project (should not be accessible) + other_project = insert(:project) + other_workflow = insert(:workflow, project: other_project) + insert(:trigger, workflow: other_workflow) + + conn = get(conn, ~p"/api/workflows") + + response = json_response(conn, 200) + + assert length(response["workflows"]) == 2 + assert %{"errors" => %{}} = response + + workflow_ids = Enum.map(response["workflows"], & &1["id"]) + assert workflow1.id in workflow_ids + assert workflow2.id in workflow_ids + refute other_workflow.id in workflow_ids + end + + test "filters workflows by project_id query parameter", %{ + conn: conn, + user: user + } do + project1 = + insert(:project, project_users: [%{user: user, role: :owner}]) + + project2 = + insert(:project, project_users: [%{user: user, role: :owner}]) + + workflow1 = insert(:workflow, project: project1, name: "P1 Workflow") + insert(:trigger, workflow: workflow1) + + workflow2 = insert(:workflow, project: project2, name: "P2 Workflow") + insert(:trigger, workflow: workflow2) + + conn = get(conn, ~p"/api/workflows?project_id=#{project1.id}") + + response = json_response(conn, 200) + + assert length(response["workflows"]) == 1 + assert List.first(response["workflows"])["id"] == workflow1.id + end + + test "nested route: lists workflows for specific project", %{ + conn: conn, + user: user + } do + project1 = + insert(:project, project_users: [%{user: user, role: :owner}]) + + project2 = + insert(:project, project_users: [%{user: user, role: :owner}]) + + workflow1 = insert(:workflow, project: project1, name: "P1 Workflow") + insert(:trigger, workflow: workflow1) + + workflow2 = insert(:workflow, project: project2, name: "P2 Workflow") + insert(:trigger, workflow: workflow2) + + conn = get(conn, ~p"/api/projects/#{project1.id}/workflows") + + response = json_response(conn, 200) + + assert length(response["workflows"]) == 1 + assert List.first(response["workflows"])["id"] == workflow1.id + refute Enum.any?(response["workflows"], &(&1["id"] == workflow2.id)) + end + + test "returns workflows with jobs, triggers, and edges", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project, name: "Full Workflow") + trigger = insert(:trigger, workflow: workflow, type: :webhook) + job = insert(:job, workflow: workflow) + + insert(:edge, + workflow: workflow, + source_trigger: trigger, + target_job: job, + condition_type: :always + ) + + conn = get(conn, ~p"/api/workflows") + + response = json_response(conn, 200) + + assert [wf] = response["workflows"] + assert wf["id"] == workflow.id + assert wf["name"] == "Full Workflow" + + assert length(wf["triggers"]) == 1 + assert List.first(wf["triggers"])["id"] == trigger.id + + assert length(wf["jobs"]) == 1 + assert List.first(wf["jobs"])["id"] == job.id + + assert length(wf["edges"]) == 1 + end + + test "returns exact workflow JSON for nested route", %{ + conn: conn, + project: project + } do + workflow = insert(:simple_workflow, name: "exact-check", project: project) + + conn = get(conn, ~p"/api/projects/#{project.id}/workflows") assert json_response(conn, 200) == %{ "errors" => %{}, - "workflows" => [ - encode_decode(workflow1), - encode_decode(workflow2) - ] + "workflows" => [encode_decode(workflow)] } end - test "returns 401 without a token", %{conn: conn} do - %{id: workflow_id, project_id: project_id} = insert(:simple_workflow) + test "returns 401 for project user cannot access", %{conn: conn} do + other_project = insert(:project) + + conn = get(conn, ~p"/api/projects/#{other_project.id}/workflows") + + assert json_response(conn, 401) + end - conn = get(conn, ~p"/api/projects/#{project_id}/workflows/#{workflow_id}") + test "returns 401 without a token" do + conn = + build_conn() + |> put_req_header("accept", "application/json") + |> get(~p"/api/workflows") assert %{"error" => "Unauthorized"} == json_response(conn, 401) end - test "returns 401 when a token is invalid", %{conn: conn} do - %{id: workflow_id, project_id: project_id} = - workflow = insert(:simple_workflow) + test "returns 401 with an invalid token" do + conn = + build_conn() + |> put_req_header("accept", "application/json") + |> Plug.Conn.put_req_header("authorization", "Bearer InvalidToken") + |> get(~p"/api/workflows") + assert json_response(conn, 401) == %{"error" => "Unauthorized"} + end + + test "returns 401 with a run token instead of API token" do + workflow = insert(:simple_workflow) workorder = insert(:workorder, dataclip: insert(:dataclip)) run = @@ -67,41 +186,71 @@ defmodule LightningWeb.API.WorkflowsControllerTest do token = Lightning.Workers.generate_run_token(run) conn = - conn + build_conn() + |> put_req_header("accept", "application/json") |> assign_bearer(token) - |> get(~p"/api/projects/#{project_id}/workflows/#{workflow_id}") + |> get(~p"/api/projects/#{workflow.project_id}/workflows/#{workflow.id}") assert json_response(conn, 401) == %{"error" => "Unauthorized"} end + end - test "returns 401 on a project the user don't have access to", %{conn: conn} do - user = insert(:user) + describe "show" do + setup [:assign_bearer_for_api, :create_project_for_current_user] - %{id: workflow_id, project_id: project_id} = insert(:simple_workflow) + test "returns a single workflow by id", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project, name: "My Workflow") + trigger = insert(:trigger, workflow: workflow, type: :webhook) + job = insert(:job, workflow: workflow, name: "Step One") + + insert(:edge, + workflow: workflow, + source_trigger: trigger, + target_job: job, + condition_type: :always + ) - conn = - conn - |> assign_bearer(user) - |> get(~p"/api/projects/#{project_id}/workflows/#{workflow_id}") + conn = get(conn, ~p"/api/workflows/#{workflow}") - assert json_response(conn, 401) == %{"error" => "Unauthorized"} + response = json_response(conn, 200) + + assert %{"workflow" => wf, "errors" => %{}} = response + assert wf["id"] == workflow.id + assert wf["name"] == "My Workflow" + assert wf["project_id"] == project.id + assert length(wf["triggers"]) == 1 + assert length(wf["jobs"]) == 1 + assert length(wf["edges"]) == 1 end - end - describe "GET /workflows/:id" do - test "returns a workflow", %{conn: conn} do - user = insert(:user) + test "returns a workflow via nested project route", %{ + conn: conn, + project: project + } do + workflow = insert(:workflow, project: project, name: "Nested Show") + insert(:trigger, workflow: workflow) - project = - insert(:project, project_users: [%{user: user}]) + conn = + get(conn, ~p"/api/projects/#{project.id}/workflows/#{workflow}") + + response = json_response(conn, 200) - %{project_id: project_id} = - workflow = insert(:simple_workflow, project: project) + assert %{"workflow" => wf, "errors" => %{}} = response + assert wf["id"] == workflow.id + assert wf["name"] == "Nested Show" + end + + test "returns exact workflow JSON for nested route", %{ + conn: conn, + project: project + } do + workflow = insert(:simple_workflow, project: project) conn = - conn - |> assign_bearer(user) - |> get(~p"/api/projects/#{project_id}/workflows/#{workflow.id}") + get(conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}") assert json_response(conn, 200) == %{ "errors" => %{}, @@ -109,47 +258,82 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 401 without a token", %{conn: conn} do - %{id: workflow_id, project_id: project_id} = insert(:simple_workflow) + test "returns 404 for non-existent workflow", %{conn: conn} do + conn = get(conn, ~p"/api/workflows/#{Ecto.UUID.generate()}") - conn = get(conn, ~p"/api/projects/#{project_id}/workflows/#{workflow_id}") + assert json_response(conn, 404) + end - assert %{"error" => "Unauthorized"} == json_response(conn, 401) + test "returns 401 for workflow in project user cannot access", %{ + conn: conn + } do + other_project = insert(:project) + + other_workflow = + insert(:workflow, project: other_project, name: "Secret") + + insert(:trigger, workflow: other_workflow) + + conn = get(conn, ~p"/api/workflows/#{other_workflow}") + + assert json_response(conn, 401) end - test "returns 422 for invalid project_id", %{conn: conn} do - user = insert(:user) + test "returns 400 for project_id mismatch on nested route", %{ + conn: conn, + user: user, + project: project + } do + other_project = + insert(:project, project_users: [%{user: user, role: :owner}]) - project = - insert(:project, project_users: [%{user: user}]) + workflow = insert(:workflow, project: project, name: "Wrong Project") + insert(:trigger, workflow: workflow) - workflow = insert(:simple_workflow, project: project) + conn = + get( + conn, + ~p"/api/projects/#{other_project.id}/workflows/#{workflow}" + ) + + response = json_response(conn, 400) + assert response["error"] == "Bad Request" + end + + test "returns 422 for invalid project_id", %{conn: conn, project: project} do + workflow = insert(:workflow, project: project) + insert(:trigger, workflow: workflow) + + conn = get(conn, ~p"/api/projects/foo/workflows/#{workflow.id}") assert %{ "errors" => %{"workflow" => ["Id foo should be a UUID."]}, "id" => nil - } == - conn - |> assign_bearer(user) - |> get(~p"/api/projects/foo/workflows/#{workflow.id}") - |> json_response(422) + } == json_response(conn, 422) + end + + test "returns 401 without a token" do + %{id: workflow_id, project_id: project_id} = insert(:simple_workflow) + + conn = + build_conn() + |> put_req_header("accept", "application/json") + |> get(~p"/api/projects/#{project_id}/workflows/#{workflow_id}") + + assert %{"error" => "Unauthorized"} == json_response(conn, 401) end end describe "POST /workflows/:project_id" do - test "inserts a workflow", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) + setup [:assign_bearer_for_api, :create_project_for_current_user] + test "inserts a workflow", %{conn: conn, project: project} do workflow = build(:simple_workflow, name: "work1", project_id: project.id) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -179,12 +363,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do Map.take(saved_workflow, [:name, :project_id]) end - test "inserts a workflow with disconnected job", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "inserts a workflow with disconnected job", %{ + conn: conn, + project: project + } do workflow = build(:simple_workflow, name: "work1", project_id: project.id) |> then(fn %{jobs: jobs} = workflow -> @@ -192,9 +374,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -206,12 +387,9 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert response_workflow == encode_decode(saved_workflow) - # Check there is still only one edge assert pluck_to_mapset(workflow.edges, [:condition_type, :enabled]) == pluck_to_mapset(saved_workflow.edges, [:condition_type, :enabled]) - # [workflow_job1, workflow_job2] = workflow.jobs - assert pluck_to_mapset(workflow.jobs, [:name, :adaptor, :body]) == pluck_to_mapset(saved_workflow.jobs, [:name, :adaptor, :body]) @@ -220,23 +398,12 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert Map.take(workflow, [:name, :project_id]) == Map.take(saved_workflow, [:name, :project_id]) - - # assert Map.take(workflow_job1, [:name, :adaptor, :body]) == - # Map.take(saved_job1, [:name, :adaptor, :body]) - - # assert Map.take(workflow_job2, [:name, :adaptor, :body]) == - # Map.take(saved_job2, [:name, :adaptor, :body]) - - # assert Map.take(hd(workflow.triggers), [:type, :enabled]) == - # Map.take(trigger, [:type, :enabled]) end - test "creates UUIDs on insert based on user arbitrary ids", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "creates UUIDs on insert based on user arbitrary ids", %{ + conn: conn, + project: project + } do workflow = build(:complex_workflow, project_id: project.id) |> then(fn %{ @@ -274,8 +441,6 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) end) - conn = assign_bearer(conn, user) - assert %{ "workflow" => response_workflow, "errors" => %{} @@ -293,12 +458,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do encode_decode(saved_workflow) |> remove_timestamps() end - test "returns 422 when an edge has invalid condition_type", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when an edge has invalid condition_type", %{ + conn: conn, + project: project + } do %{edges: [edge | _edges]} = workflow = build(:simple_workflow, name: "work1", project_id: project.id) @@ -307,9 +470,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -324,12 +486,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when a job has invalid value", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when a job has invalid value", %{ + conn: conn, + project: project + } do %{jobs: [job1, job2 | _jobs]} = workflow = build(:complex_workflow, name: "work1", project_id: project.id) @@ -344,9 +504,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -365,12 +524,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do ]) end - test "returns 422 when a trigger has invalid value", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when a trigger has invalid value", %{ + conn: conn, + project: project + } do %{triggers: [trigger]} = workflow = build(:complex_workflow, name: "work1", project_id: project.id) @@ -379,9 +536,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -396,12 +552,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } == json_response(conn, 422) end - test "returns 422 when workflow limit has been reached", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when workflow limit has been reached", %{ + conn: conn, + project: project + } do insert(:simple_workflow, name: "work1", project: project) workflow = @@ -417,9 +571,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do ) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -432,12 +585,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } = json_response(conn, 422) end - test "returns 422 when there are too many active triggers", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when there are too many active triggers", %{ + conn: conn, + project: project + } do workflow = build(:simple_workflow, name: "workflow", @@ -446,9 +597,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do ) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -463,23 +613,21 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } = json_response(conn, 422) end - test "returns 422 on project id mismatch", %{conn: conn} do - user = insert(:user) - - project1 = - insert(:project, project_users: [%{user: user}]) - - project2 = - insert(:project, project_users: [%{user: user}]) + test "returns 422 on project id mismatch", %{ + conn: conn, + user: user, + project: project + } do + other_project = + insert(:project, project_users: [%{user: user, role: :owner}]) workflow = - build(:simple_workflow, name: "work1", project_id: project1.id) + build(:simple_workflow, name: "work1", project_id: project.id) conn = - conn - |> assign_bearer(user) - |> post( - ~p"/api/projects/#{project2.id}/workflows", + post( + conn, + ~p"/api/projects/#{other_project.id}/workflows", Jason.encode!(workflow) ) @@ -493,12 +641,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when graph has a cycle", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when graph has a cycle", %{ + conn: conn, + project: project + } do workflow = build(:complex_workflow, name: "work1", project_id: project.id) |> then(fn %{jobs: jobs, edges: edges} = workflow -> @@ -514,9 +660,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows", Jason.encode!(workflow) ) @@ -531,12 +676,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when there is a duplicated id", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when there is a duplicated id", %{ + conn: conn, + project: project + } do %{id: edge_id} = insert(:edge) workflow = @@ -549,9 +692,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows/", Jason.encode!(workflow) ) @@ -566,12 +708,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } == json_response(conn, 422) end - test "returns 422 when edges misses a source trigger", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when edges misses a source trigger", %{ + conn: conn, + project: project + } do trigger = build(:trigger, type: :webhook, enabled: true) @@ -592,9 +732,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows", Jason.encode!(workflow) ) @@ -607,12 +746,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when edges has multiple source triggers", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when edges has multiple source triggers", %{ + conn: conn, + project: project + } do trigger = build(:trigger, type: :webhook, enabled: true) @@ -641,9 +778,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows", Jason.encode!(workflow) ) @@ -659,13 +795,9 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end test "returns 422 when an edge source_job_id points to a trigger", %{ - conn: conn + conn: conn, + project: project } do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - workflow = build(:simple_workflow, project_id: project.id) |> then(fn %{jobs: [job1], triggers: [trigger]} = workflow -> @@ -685,9 +817,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> post( + post( + conn, ~p"/api/projects/#{project.id}/workflows", Jason.encode!(workflow) ) @@ -702,27 +833,22 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 401 without a token", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 401 without a token", %{project: project} do build(:simple_workflow, name: "work1", project: project) - conn = post(conn, ~p"/api/projects/#{project.id}/workflows/") + conn = + build_conn() + |> put_req_header("accept", "application/json") + |> post(~p"/api/projects/#{project.id}/workflows/") assert %{"error" => "Unauthorized"} == json_response(conn, 401) end end describe "PATCH /workflows/:workflow_id" do - test "updates a workflow trigger", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) + setup [:assign_bearer_for_api, :create_project_for_current_user] + test "updates a workflow trigger", %{conn: conn, project: project} do %{edges: [edge1 | other_edges], triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -740,9 +866,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do ) conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -763,12 +888,7 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> remove_timestamps() end - test "adds some jobs to a workflow", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "adds some jobs to a workflow", %{conn: conn, project: project} do %{edges: edges, jobs: jobs} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -794,9 +914,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -817,12 +936,7 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> remove_timestamps() end - test "Adds a disconnected/orphan job", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "Adds a disconnected/orphan job", %{conn: conn, project: project} do %{jobs: jobs} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -837,9 +951,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -870,19 +983,15 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> remove_timestamps() end - test "returns 404 when the workflow doesn't exist", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 404 when the workflow doesn't exist", %{ + conn: conn, + project: project + } do unexisting_id = Ecto.UUID.generate() patch = %{name: "work1.1"} assert %{"id" => ^unexisting_id, "errors" => ["Not Found"]} = conn - |> put_req_header("content-type", "application/json") - |> assign_bearer(user) |> patch( ~p"/api/projects/#{project.id}/workflows/#{unexisting_id}", Jason.encode!(patch) @@ -927,12 +1036,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert Presence.has_any_presence?(workflow) end - test "returns 422 for invalid triggers patch", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 for invalid triggers patch", %{ + conn: conn, + project: project + } do %{triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -946,9 +1053,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -963,12 +1069,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 for invalid jobs patch", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 for invalid jobs patch", %{ + conn: conn, + project: project + } do %{jobs: [job | other_jobs]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -982,9 +1086,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -997,12 +1100,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 for invalid edges patch", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 for invalid edges patch", %{ + conn: conn, + project: project + } do %{edges: [edge]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -1016,9 +1117,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -1033,12 +1133,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 on project id mismatch", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 on project id mismatch", %{ + conn: conn, + project: project + } do workflow = insert(:simple_workflow, name: "work1", project: project) |> Repo.reload() @@ -1047,9 +1145,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do patch = %{project_id: Ecto.UUID.generate()} conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -1064,12 +1161,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when trying to replace the triggers", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when trying to replace the triggers", %{ + conn: conn, + project: project + } do workflow = insert(:simple_workflow, name: "work1", project: project) |> Repo.reload() @@ -1087,9 +1182,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -1104,12 +1198,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when workflow limit has been reached", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when workflow limit has been reached", %{ + conn: conn, + project: project + } do insert(:simple_workflow, name: "work1", project: project) trigger = build(:trigger, enabled: false) @@ -1131,9 +1223,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do ) conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -1146,12 +1237,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } == json_response(conn, 422) end - test "returns 422 when there are too many enabled triggers", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when there are too many enabled triggers", %{ + conn: conn, + project: project + } do workflow = insert(:simple_workflow, name: "work1", project: project) |> Repo.reload() @@ -1170,9 +1259,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } conn = - conn - |> assign_bearer(user) - |> patch( + patch( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(patch) ) @@ -1187,30 +1275,26 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 401 without a token", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 401 without a token", %{project: project} do workflow = insert(:simple_workflow, name: "work1", project: project) conn = - patch(conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", %{ - name: "work-2" - }) + build_conn() + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") + |> patch( + ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", + Jason.encode!(%{name: "work-2"}) + ) assert %{"error" => "Unauthorized"} == json_response(conn, 401) end end describe "PUT /workflows/:workflow_id" do - test "updates completely a workflow", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) + setup [:assign_bearer_for_api, :create_project_for_current_user] + test "updates completely a workflow", %{conn: conn, project: project} do %{triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -1241,7 +1325,6 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert %{"workflow" => response_workflow, "errors" => %{}} = conn - |> assign_bearer(user) |> put( ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(complete_update) @@ -1261,12 +1344,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> remove_timestamps() == saved_workflow end - test "updates completely a workflow with disconnected job", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "updates completely a workflow with disconnected job", %{ + conn: conn, + project: project + } do %{triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -1295,9 +1376,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> put( + put( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(complete_update) ) @@ -1322,12 +1402,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> Enum.count() == 2 end - test "updates completely a workflow removing a job", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "updates completely a workflow removing a job", %{ + conn: conn, + project: project + } do workflow = insert(:complex_workflow, name: "work1.0", project: project) |> Repo.reload() @@ -1351,7 +1429,6 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert %{"workflow" => response_workflow, "errors" => %{}} = conn - |> assign_bearer(user) |> put( ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(complete_update) @@ -1375,12 +1452,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do MapSet.new(saved_workflow.edges, & &1.id) end - test "updates workflow ignoring workflow_id", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "updates workflow ignoring workflow_id", %{ + conn: conn, + project: project + } do %{triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -1421,7 +1496,6 @@ defmodule LightningWeb.API.WorkflowsControllerTest do "errors" => %{} } = conn - |> assign_bearer(user) |> put( ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(complete_update_external_ref) @@ -1431,19 +1505,15 @@ defmodule LightningWeb.API.WorkflowsControllerTest do assert_response(response_workflow) end - test "returns 404 when the workflow doesn't exist", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 404 when the workflow doesn't exist", %{ + conn: conn, + project: project + } do unexisting_id = Ecto.UUID.generate() workflow = build(:simple_workflow, project_id: project.id) assert %{"id" => ^unexisting_id, "errors" => ["Not Found"]} = conn - |> put_req_header("content-type", "application/json") - |> assign_bearer(user) |> put( ~p"/api/projects/#{project.id}/workflows/#{unexisting_id}", Jason.encode!(Map.put(workflow, :id, unexisting_id)) @@ -1491,13 +1561,9 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end test "returns 404 when the workflow id doesn't match the one on the path", %{ - conn: conn + conn: conn, + project: project } do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - unexisting_id = Ecto.UUID.generate() workflow = build(:simple_workflow, project_id: project.id) @@ -1508,8 +1574,6 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } } = conn - |> put_req_header("content-type", "application/json") - |> assign_bearer(user) |> put( ~p"/api/projects/#{project.id}/workflows/#{unexisting_id}", Jason.encode!(workflow) @@ -1517,12 +1581,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do |> json_response(422) end - test "returns 422 when one id belongs to another workflow", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when one id belongs to another workflow", %{ + conn: conn, + project: project + } do %{triggers: [trigger]} = workflow = insert(:simple_workflow, name: "work1.0", project: project) @@ -1558,9 +1620,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> put( + put( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(complete_update_external_id) ) @@ -1575,12 +1636,10 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 422 when trying to replace the triggers", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 422 when trying to replace the triggers", %{ + conn: conn, + project: project + } do workflow = insert(:simple_workflow, name: "work1.0", project: project) |> Repo.reload() @@ -1608,9 +1667,8 @@ defmodule LightningWeb.API.WorkflowsControllerTest do end) conn = - conn - |> assign_bearer(user) - |> put( + put( + conn, ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(invalid_update) ) @@ -1625,17 +1683,14 @@ defmodule LightningWeb.API.WorkflowsControllerTest do } end - test "returns 401 without a token", %{conn: conn} do - user = insert(:user) - - project = - insert(:project, project_users: [%{user: user}]) - + test "returns 401 without a token", %{project: project} do workflow = insert(:simple_workflow, name: "work1", project: project) conn = - put( - conn, + build_conn() + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") + |> put( ~p"/api/projects/#{project.id}/workflows/#{workflow.id}", Jason.encode!(%{workflow | name: "work2"}) )