test/openagents_web/release_paths_test.exs

58e6347eeb72 · 2 KB

defmodule OpenAgentsWeb.ReleasePathsTest do
  @moduledoc """
  A path to a `priv` file must be resolved when it is used, not when the module
  is compiled.

  `Application.app_dir/2` and `:code.priv_dir/1` answer with the path of the
  application *as currently loaded*. Evaluated in a module attribute, that is
  the build machine's `_build` directory, and it is frozen into the compiled
  module. A release is built in one container and run in another, so the frozen
  path does not exist where the code actually runs.

  The failure is invisible in every environment that compiles and runs in the
  same place, which is every environment a test suite runs in. So this is a
  source-level check rather than a behavioural one: `/api/contracts/repositories-v1.json`
  answered 200 in development and 500 in production and staging both, and no
  test could have told the difference.
  """

  use OpenAgentsWeb.ConnCase, async: true

  # `@name Application.app_dir(...)` and `@name :code.priv_dir(...)`, including
  # the wrapped form the formatter produces for a long call.
  @baked ~r/^[ \t]*@[a-z_]+[ \t]+(?:Application\.app_dir|:code\.priv_dir)\b/m

  test "no module attribute resolves an application path at compile time" do
    offenders =
      for path <- Path.wildcard("lib/**/*.ex"),
          source = File.read!(path),
          [match] <- Regex.scan(@baked, source),
          do: {path, String.trim(match)}

    assert offenders == [], """
    These resolve an application directory into a module attribute, which
    freezes the build machine's path into the compiled module. The release runs
    somewhere else, so the file is not there and reading it raises:

    #{Enum.map_join(offenders, "\n", fn {file, line} -> "  #{file}\n    #{line}" end)}

    Move the call into a function so it is resolved when it is used.
    """
  end

  describe "the contract endpoint" do
    test "serves the contract with an etag", %{conn: conn} do
      conn = get(conn, ~p"/api/contracts/repositories-v1.json")

      assert response = response(conn, 200)
      assert {:ok, decoded} = Jason.decode(response)
      assert is_map(decoded)

      assert ["public, max-age=300"] = get_resp_header(conn, "cache-control")
      assert [etag] = get_resp_header(conn, "etag")
      assert etag =~ ~r/^"[0-9a-f]{64}"$/
    end
  end
end