elixir-plug / plug

Compose web applications with functions
https://hex.pm/packages/plug
Other
2.84k stars 582 forks source link

Add Plug.Test.sent_chunks/2 #1160

Open wojtekmach opened 1 year ago

wojtekmach commented 1 year ago

Before this patch there was no way to reconstruct the invidual chunks that were sent. All we got was the full resulting body in conn.resp_body.

For completeness, I believe instead of messaging we could store chunks in a list in the test adapter state and simply append:

- def send_chunked(state, _status, _headers), do: {:ok, "", %{state | chunks: ""}}
+ def send_chunked(state, _status, _headers), do: {:ok, "", %{state | chunks: []}}

- def chunk(%{owner: owner, ref: ref, chunks: chunks} = state, chunk) do
-   send(owner, {ref, :chunk, chunk})
-   body = chunks <> IO.iodata_to_binary(chunk)
-   {:ok, body, %{state | chunks: body}}
- end
+ def chunk(%{owner: owner, ref: ref, chunks: chunks} = state, chunk) do
+   chunk = IO.iodata_to_binary(chunk)
+   body = IO.iodata_to_binary([chunks, chunk])
+   {:ok, body, %{state | chunks: chunks ++ [chunk]}}
+ end

but I was following the existing functions sent_informs and sent_upgrades.

My use case is to be able to test response streaming using Req :plug adapter:

req =
  Req.new(
    plug: fn conn ->
      conn = Plug.Conn.send_chunked(conn, 200)
      {:ok, conn} = Plug.Conn.chunk(conn, "foo")
      {:ok, conn} = Plug.Conn.chunk(conn, "bar")
      conn
    end,
    into: []
  )

resp = Req.request!(req)
assert resp.body == ["foo", "bar"]
wojtekmach commented 1 year ago

Oh no, I just realised there's a Plug.Conn.register_before_chunk/2 on main which I can totally use instead. Sorry about that!

wojtekmach commented 11 months ago

Given register_before_chunk got reverted, can I pursue this?

josevalim commented 11 months ago

Sounds good to me!

wojtekmach commented 6 months ago

Turns out process messaging isn't a great fit for my main use case for this which is Req :plug adapter. The reason is if someone ends up with something like this:

test "foo" do
  defmodule Foo do
    use GenServer

    def start_link(arg) do
      GenServer.start_link(__MODULE__, arg)
    end

    @impl true
    def init(_) do
      Req.get!(
        plug: fn conn ->
          Plug.Conn.send_resp(conn, 200, "ok")
        end
      )

      {:ok, %{}}
    end
  end

  start_supervised!(Foo)
  Process.sleep(100)
end

The GenServer ends up receiving these Plug.Test messages which is unexpected. This was reported in https://github.com/wojtekmach/req/issues/316 and I fixed it in Req by consuming these.

In any case, I'd be still keen on a Plug.Test.sent_chunks function that receives accumulated chunks stored in the adapter state as mentioned in the PR description.