4.2 KiB · text History 6b5083e
defmodule GitGud.Workflows.ExprTest do
  use ExUnit.Case, async: true

  alias GitGud.Workflows.Expr

  @contexts %{
    "github" => %{
      "workflow" => "ci",
      "ref" => "refs/heads/main",
      "head_ref" => "",
      "run_id" => "42",
      "event" => %{"pull_request" => %{"number" => 7}, "forced" => false}
    },
    "inputs" => %{"env" => "prod"},
    "vars" => %{"REGION" => "eu"},
    "matrix" => %{"target" => "arm64", "n" => 2},
    "needs" => %{"build" => %{"result" => "success", "outputs" => %{"tag" => "v1"}}}
  }

  defp i(string), do: Expr.interpolate(string, @contexts)

  describe "the patterns concurrency groups are made of" do
    test "workflow and ref" do
      assert i("${{ github.workflow }}-${{ github.ref }}") == {:ok, "ci-refs/heads/main"}
    end

    test "a pull request's number, else the ref" do
      assert i("${{ github.event.pull_request.number || github.ref }}") == {:ok, "7"}

      assert Expr.interpolate(
               "${{ github.event.pull_request.number || github.ref }}",
               put_in(@contexts, ["github", "event"], %{})
             ) == {:ok, "refs/heads/main"}
    end

    test "head_ref, else the run id: an empty string is falsy" do
      assert i("${{ github.head_ref || github.run_id }}") == {:ok, "42"}
    end

    test "inputs, vars, matrix and needs" do
      assert i("deploy-${{ inputs.env }}") == {:ok, "deploy-prod"}
      assert i("${{ vars.REGION }}") == {:ok, "eu"}
      assert i("${{ matrix.target }}-${{ matrix.n }}") == {:ok, "arm64-2"}
      assert i("${{ needs.build.outputs.tag }}") == {:ok, "v1"}
    end

    test "index syntax, case-insensitively" do
      assert i("${{ inputs['env'] }}") == {:ok, "prod"}
      assert i("${{ GitHub.Workflow }}") == {:ok, "ci"}
      assert i("${{ github['event']['pull_request'].number }}") == {:ok, "7"}
    end

    test "format" do
      assert i("${{ format('{0}-{1}', github.workflow, inputs.env) }}") == {:ok, "ci-prod"}
      assert i("${{ format('{{literal}} {0}', 'x') }}") == {:ok, "{literal} x"}
    end

    test "text without an expression is itself" do
      assert i("deploy-prod") == {:ok, "deploy-prod"}
    end
  end

  describe "operators and values" do
    test "|| falls back over null and empty string, && keeps the falsy operand" do
      assert i("${{ github.nothing || 'fallback' }}") == {:ok, "fallback"}
      assert i("${{ '' || null || 'x' }}") == {:ok, "x"}
      assert i("${{ github.head_ref && 'yes' }}") == {:ok, ""}
      assert i("${{ github.workflow && 'yes' }}") == {:ok, "yes"}
    end

    test "== and != compare strings ignoring case, and loosely across types" do
      assert Expr.evaluate("github.ref == 'REFS/HEADS/MAIN'", @contexts) == {:ok, true}
      assert Expr.evaluate("github.ref != 'refs/heads/main'", @contexts) == {:ok, false}
      assert Expr.evaluate("github.run_id == 42", @contexts) == {:ok, true}
      assert Expr.evaluate("null == 0", @contexts) == {:ok, true}
      assert Expr.evaluate("'abc' == 0", @contexts) == {:ok, false}
    end

    test "!, parentheses and precedence" do
      assert Expr.evaluate("!github.event.forced", @contexts) == {:ok, true}
      assert Expr.evaluate("!(true && false) || false", @contexts) == {:ok, true}
      assert Expr.evaluate("false || true && false", @contexts) == {:ok, false}
    end

    test "values stringify as GitHub does" do
      assert i("${{ null }}|${{ true }}|${{ false }}|${{ 1.0 }}|${{ 0x10 }}|${{ 'it''s' }}") ==
               {:ok, "|true|false|1|16|it's"}
    end

    test "a path through something missing is null" do
      assert i("${{ github.event.nope.deeper }}") == {:ok, ""}
    end
  end

  describe "what isn't supported is an error, not a guess" do
    test "other functions" do
      assert i("${{ toJSON(github) }}") == :error
      assert i("${{ contains(github.ref, 'main') }}") == :error
    end

    test "a context nobody supplied" do
      assert i("${{ env.FOO }}") == :error
    end

    test "unknown syntax" do
      assert i("${{ github.* }}") == :error
      assert i("${{ github.ref < 'x' }}") == :error
      assert i("${{ \"double\" }}") == :error
      assert i("${{ github.ref") == :error
    end

    test "an object where a string was wanted" do
      assert i("${{ github.event }}") == :error
    end
  end
end