Skip to main content

lib/cuerdo.ex

defmodule Cuerdo do
  @moduledoc """
  Cuerdo is an [Arazzo](https://spec.openapis.org/arazzo/v1.1.0) workflow runner and automatic property-based testing tool written in Elixir.

  Arazzo is a community-driven specification within the OpenAPI Initiative, a project part of the
  Linux Foundation Collaborative.
  OpenAPI describes REST APIs, and Arazzo specifies how to combine multiple individual OpenAPI
  endpoints as a "workflow". Workflows consist of one or more steps, where each step consists of
  either an HTTP request referencing an OpenAPI endpoint, or another workflow. For more information,
  refer to the [Arazzo official site](https://www.openapis.org/arazzo-specification).

  One of Cuerdo's primary goals is reducing the amount of hand-written integration tests
  required to validate API workflows.
  When used with `Cuerdo.ArazzoCase`, workflow inputs are automatically generated from the
  JSON Schemas defined in the Arazzo document.
  Each workflow is then executed multiple times with different generated inputs.

  In addition to evaluating Arazzo successCriteria, Cuerdo validates that requests and responses
  conform to the OpenAPI contract associated with each operation.
  This includes schema validation, content type validation, required parameter checks,
  and response status code verification.

  As a result, a single Arazzo document can serve both as executable workflow documentation
  and as a property-based test suite.

  ## Quick Start
  Define a [`arazzo_document_test`](`Cuerdo.ArazzoCase.arazzo_document_test/1`) in your test module
  ```elixir
  defmodule MyTest do
    use Cuerdo.ArazzoCase

    arazzo_document_test document: YamlElixir.read_from_file!("arazzo.yaml")
  end
  ```

  Or execute a workflow directly
  ```elixir
  iex> inputs = %{"email" => "user@example.com", "password" => "securePassword"}
  iex> document = YamlElixir.read_from_file!("arazzo.yaml")
  iex> {:ok, context} = Cuerdo.Arazzo.run_workflow(inputs, "createUserWorkflow", document)
  iex> Cuerdo.Arazzo.Context.workflow_outputs(context, "createUserWorkflow")
  %{"token" => "userSessionToken"}
  ```

  ## Usage
  Consider the following simple workflow. A single GET request with query parameters that queries
  "people" entities, and validates that the returned object matches the input filters

  ```yaml
  # specs/arazzo.yaml
  - workflowId: getPeople
    inputs:
      type: object
      additionalProperties: false
      required: ["name"]
      properties:
        name:
          type: string
          minLength: 1
    steps:
      - stepId: getPeopleStep
        operationId: getPeople
        parameters:
          - name: name
            in: query
            value: $inputs.name
        successCriteria:
          - condition: $statusCode == 200
          - type: regex
            context: $response.body#/0/name
            condition: "^$inputs.name"
        outputs:
          firstMatchingName: $response.body#/0/name
    ```

  ### Directly in code
  You can run the workflow directly with specific inputs via `Cuerdo.Arazzo.run_workflow/3`, for
  example

  ```elixir
  inputs = %{"name" => "John"}
  arazzo_document = YamlElixir.read_from_file!("path/to/arazzo.yaml")
  {:ok, context} = Cuerdo.Arazzo.run_workflow(inputs, "getPeople", arazzo_document)
  ```

  On success, the function returns a `t:Cuerdo.Arazzo.Context.t/0` containing workflow outputs,
  step outputs, and request/response data for each executed step.

  ```elixir
  iex> Cuerdo.Arazzo.Context.step_outputs(context, "getPeople", "getPeopleStep")
  %{"firstMatchingName" => "Johnathan"}
  ```

  ### ArazzoCase
  You can execute the workflow(s) as part of a test suite, using `Cuerdo.ArazzoCase`.
  Consider the same Arazzo workflow from [above](#module-usage), you can define a test module
  as follows
  ```elixir
  # test/myapp/get_people_test.exs
  defmodule MyApp.GetPeopleTest do
    use Cuerdo.ArazzoCase

    arazzo_document_test document: YamlElixir.read_from_file!("path/to/arazzo.yaml")
  end
  ```
  The `Cuerdo.ArazzoCase.arazzo_document_test/1` macro generates a test for each workflow with random
  inputs generated by `RockSolid.from_schema/2`. To exclude specific workflows, apply transformation
  functions, and other options refer to [arazzo_document_test](`Cuerdo.ArazzoCase.arazzo_document_test/1`)

  ## Validations
  On top of the validations defined by `successCriteria` field, the following non-Arazzo validations
  are performed at every step:
  - The workflow inptus match the `inputs` schema in the Arazzo document
  - The request Content-Type header matches any of the Content-Type defined in the OpenAPI operation
  - The request body matches the schema defined in the OpenAPI operation
  - All required parameters defined in the OpenAPI operation are present in the step definition
  - The response body matches any of the schemas defined in the OpenAPI operation based on the
  Content-Type response header and response status code
  """
end