phoenix_ecto

repository·main·Indexed 20 days ago

https://github.com/phoenixframework/phoenix_ecto

Provides the glue between the Phoenix web framework and the Ecto database wrapper. It implements essential protocols for Ecto.Changeset in forms, Decimal in HTML, and Ecto exceptions in Plugs. It includes tools for concurrent browser acceptance tests via Phoenix.Ecto.SQL.Sandbox, support for Ecto types in HTML input mapping, and automated handling of PendingMigrationError and StorageNotCreatedError.

Tokens
3.8K
Snippets
15
Records
22
Agent score
68%

What's inside phoenix_ecto

  1. Enable concurrent browser tests with Phoenix.Ecto.SQL.Sandbox

    main

    To run acceptance tests (using tools like Hound or Wallaby) concurrently with a headless browser, use the Phoenix.Ecto.SQL.Sandbox plug. This requires PostgreSQL.

    1. Set a configuration flag in config/test.exs:

      config :your_app, sql_sandbox: true
    2. Conditionally add the plug to your Endpoint in lib/your_app/endpoint.ex. It must be placed before any plug that accesses the database (e.g., plug YourApp.Router).

    if Application.get_env(:your_app, :sql_sandbox) do
      plug Phoenix.Ecto.SQL.Sandbox
    end
  2. Exclude Ecto exceptions from Plug implementation

    main

    By default, phoenix_ecto implements the Plug.Exception protocol for relevant Ecto exceptions. You can disable this for specific exceptions by adding them to the exclude_ecto_exceptions_from_plug list in your phoenix_ecto configuration.

    config :phoenix_ecto,
      exclude_ecto_exceptions_from_plug: [Ecto.NoResultsError]
  3. Use inputs_for with Ecto changesets

    main

    When working with nested data like embeds_one, embeds_many, has_one, has_many, belongs_to, or many_to_many, you can use inputs_for/3 to generate a list of forms for the associated data.

    Important Requirements:

    • Preload Associations: If you are using inputs_for for an association (e.g., belongs_to or has_many), you must preload the association in your query before passing the changeset to the form. If the association is not loaded, an ArgumentError will be raised.
    • No :default option: The :default option is not supported when using inputs_for with changesets. Default values must be set within the changeset data itself.
    # Example usage in a Phoenix template
    <%= inputs_for @changeset, :address do |address_form| %>
      <%= text_input address_form, :street %>
      <%= text_input address_form, :city %>
    <% end %>
  4. Automatic validation attribute generation

    main

    Phoenix extracts validation constraints from the Ecto.Changeset to populate HTML attributes.

    • Required fields: Adds the required attribute.
    • Length validations: :length validations are converted to maxlength and minlength attributes.
    • Number validations: :number validations are converted to step, min, and max attributes. For :integer types, the step defaults to 1.

    Note: For integer types, less_than is mapped to max: value - 1 and greater_than is mapped to min: value + 1 to ensure valid integer input.

  5. Support LiveViews in acceptance tests

    main

    LiveViews can be supported by using an on_mount hook to extract sandbox metadata from the connection info.

    1. Declare connection info in your LiveSocket configuration (endpoint.ex):

      socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:user_agent, session: @session_options]]
    2. Create an on_mount hook to assign and allow the sandbox:

      defmodule MyApp.LiveAcceptance do
        import Phoenix.LiveView
      
        def on_mount(:default, _params, _session, socket) do
          socket = assign_new(socket, :phoenix_ecto_sandbox, fn ->
            if connected?(socket), do: get_connect_info(socket, :user_agent)
          end)
      
          metadata = socket.assigns.phoenix_ecto_sandbox
          Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
          {:cont, socket}
        end
      end
    3. Apply the hook in your web.ex file (conditionally based on your sandbox config):

      def live_view do
        quote do
          use Phoenix.LiveView
          if Application.compile_env(:your_app, :sql_sandbox) do
            on_mount MyApp.LiveAcceptance
          end
        end
      end

    Note: If using live_session with other on_mount hooks (like authentication), ensure MyApp.LiveAcceptance runs before them so subsequent hooks have access to the sandbox.

  6. Support Channels in acceptance tests

    main

    To allow Channels to access the sandboxed database connection, you must pass connection metadata through the socket.

    1. Declare connection info in your socket configuration (endpoint.ex). You can use :user_agent or a custom header via :x_headers:

      socket "/path", Socket, websocket: [connect_info: [:user_agent]]
      # OR
      socket "/path", Socket, websocket: [connect_info: [:x_headers]]
    2. Capture the metadata in the connect/3 callback of your Socket module:

      def connect(_params, socket, connect_info) do
        {:ok, assign(socket, :phoenix_ecto_sandbox, connect_info.user_agent)}
      end
    3. Allow the sandbox in your Channel (e.g., in join/3). It is recommended to use a helper function:

      def join("room:lobby", _payload, socket) do
        allow_ecto_sandbox(socket)
        {:ok, socket}
      end
      
      defp allow_ecto_sandbox(socket) do
        Phoenix.Ecto.SQL.Sandbox.allow(
          socket.assigns.phoenix_ecto_sandbox,
          Ecto.Adapters.SQL.Sandbox
        )
      end
  7. Enable concurrent tests for external HTTP clients

    main

    You can expose a sandbox route that allows external clients (like JavaScript test suites) to manage sandbox sessions via HTTP requests.

    1. Add the plug to your Endpoint with :at and :repo options:

      plug Phoenix.Ecto.SQL.Sandbox,
            at: "/sandbox",
            repo: MyApp.Repo

      This exposes:

      • POST /sandbox: Spawns a new sandbox session. Returns serialized metadata in the response body.
      • DELETE /sandbox: Stops the active sandbox session.
    2. Client Requirements:

      • For POST, the client must capture the response body and send it back in the user-agent header (or the custom header specified by :header) for subsequent requests.
      • For DELETE, the client must provide the metadata in the header.
    3. Repository Mode: Ensure your repository mode is set to :manual or {:shared, self()} before the client starts. This is typically done in test/test_helper.exs:

      Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)
    plug Phoenix.Ecto.SQL.Sandbox,
      at: "/sandbox",
      repo: MyApp.Repo,
      timeout: 15_000
  8. Setup Phoenix.Ecto.SQL.Sandbox for concurrent acceptance tests

    main

    To enable concurrent, transactional acceptance tests using Ecto.Adapters.SQL.Sandbox, follow these steps:

    1. Enable the flag in your test configuration (config/test.exs):

      config :your_app, sql_sandbox: true
    2. Conditionally add the plug to your Endpoint (lib/your_app/endpoint.ex). It must be at the top of the plug list, before any other plugs:

      if Application.compile_env(:your_app, :sql_sandbox) do
        plug Phoenix.Ecto.SQL.Sandbox
      end
    3. In your acceptance test setup, start the sandbox owner and retrieve the metadata to pass to your test client (e.g., Wallaby):

      setup tags do
        pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
        on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
        metadata_header = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
        # Pass metadata_header to your acceptance test library
        :ok
      end
    config :your_app, sql_sandbox: true
    
    # In endpoint.ex
    if Application.compile_env(:your_app, :sql_sandbox) do
      plug Phoenix.Ecto.SQL.Sandbox
    end
  9. Configure concurrent acceptance tests with Wallaby

    main

    Wallaby supports concurrent testing via ChromeDriver and Selenium.

    Option 1: Using Wallaby.Feature (Recommended) If you use use Wallaby.Feature in your test module, Wallaby handles the Ecto Sandbox setup automatically.

    defmodule MyAppWeb.PageFeature do
      use ExUnit.Case, async: true
      use Wallaby.Feature
    
      feature "shows some text", %{session: session} do
        session
        |> visit("/home")
        |> assert_text("Hello world!")
      end
    end

    Option 2: Manual Setup If you use use Wallaby.DSL, you must manually start the owner and pass the metadata to Wallaby.start_session/1.

    use Wallaby.DSL
    
    setup tags do
      pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
      on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
      metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
      {:ok, session} = Wallaby.start_session(metadata: metadata)
    end
  10. Configure concurrent acceptance tests with Hound

    main

    To use Hound for concurrent acceptance testing, add :hound to your dependencies and start it in test/test_helper.exs. In your test case, use Phoenix.Ecto.SQL.Sandbox.metadata_for/2 to pass the sandboxed connection metadata to Hound.

    # In test/test_helper.exs
    {:ok, _} = Application.ensure_all_started(:hound)
    
    # In your test case
    use Hound.Helpers
    
    setup tags do
      pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
      on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
      metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
      Hound.start_session(metadata: metadata)
      :ok
    end
  11. Exclude Ecto exceptions from the Plug implementation

    main

    By default, phoenix_ecto implements Plug.Exception for several Ecto and Postgrex errors to automatically map them to appropriate HTTP status codes. If you want to prevent certain exceptions from being handled by this Plug (for example, to handle them manually in your own error handlers), you can add them to the :exclude_ecto_exceptions_from_plug list in your application configuration.

    Supported exceptions that can be excluded include:

    • Ecto.CastError (maps to 400)
    • Ecto.Query.CastError (maps to 400)
    • Ecto.NoResultsError (maps to 404)
    • Ecto.StaleEntryError (maps to 409)
    • Ecto.SubQueryError (delegates status to the underlying exception)
    • Phoenix.Ecto.PendingMigrationError (maps to 503)
    • Phoenix.Ecto.StorageNotCreatedError (maps to 503)
    • Postgrex.Error (maps to 400 for :character_not_in_repertoire, otherwise 500)
    # Example configuration in config/config.exs
    config :phoenix_ecto, 
      exclude_ecto_exceptions_from_plug: [Ecto.NoResultsError]