Hey all, it’s time for Yet Another DB OwnershipError thread!
I’ve been working through @germsvel’s fantastic TDD Phoenix Book, but I’ve applied modern testing facilities and options (i.e. Wallaby.Feature), Verified Routes, Phoenix LiveView and German’s own PhoenixTest
library, and have run into some problems when I added a database store for my sessions.
I went with database sessions to skirt around the fact that LiveViews cannot modify the cookie based sessions directly, and I’m experimenting with a fully LiveView auth setup.
I currently use this technique in a production application, but instead it uses stock testing methods as well as PhoenixTest, and about 20 times as many feature tests.
Unplug the database sessions from the test bench and voila, it works.
I’m really scratching my head here as my test bench is not so different in configuration from the production app, other than using Wallaby.
Basically, I’ve setup the tests as Wallaby’s documentation suggests for Phoenix and LiveView:
endpoint.ex
if sandbox = Application.compile_env(:chatter, :sandbox) do
plug Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox
end
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [:user_agent, session: @session_options]],
longpoll: [connect_info: [:user_agent, session: @session_options]]
config/test.exs
config :wallaby,
driver: Wallaby.Chrome,
otp_app: :chatter
config :chatter, :sandbox, Ecto.Adapters.SQL.Sandbox
test_helpers.exs
{:ok, _} = Application.ensure_all_started(:ex_machina)
{:ok, _} = Application.ensure_all_started(:wallaby)
Application.put_env(:wallaby, :base_url, ChatterWeb.Endpoint.url())
on_mount
hooks for LiveView:
def on_mount(:default, _, session, socket) do
allow_ecto_sandbox(socket)
end
defp allow_ecto_sandbox(socket) do
%{assigns: %{phoenix_ecto_sandbox: metadata}} =
assign_new(socket, :phoenix_ecto_sandbox, fn ->
get_connect_info(socket, :user_agent)
end)
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Application.get_env(:chatter, :sandbox))
end
feature_case.ex
(RTXasync on by default)
using do
quote do
use Wallaby.Feature
use ChatterWeb, :verified_routes
import Chatter.DataCase
import Chatter.Factory
use ExUnit.Case, async: true
end
end
And of course, a test! This issue is causing all tests to fail but here is an example:
test "user can create a new room successfully", %{session: session} do
user = build(:user) |> set_password() |> insert()
params = params_for(:room)
session
|> visit(~p"/")
|> sign_in(as: user)
|> click(Query.link("New Room"))
|> fill_in(Query.text_field("Name"), with: params.name)
|> click(Query.button("Save"))
|> assert_has(Query.data("role", "title", text: "Chatting in #{params.name}"))
end
where sign_in/2
is just some more Wallaby actions as outlined in the TDD Phoenix book.
The stack trace for the ownership error points right at my session plug:
(ecto_sql 3.11.1) lib/ecto/adapters/sql.ex:1051: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto_sql 3.11.1) lib/ecto/adapters/sql.ex:952: Ecto.Adapters.SQL.execute/6
(ecto 3.11.2) lib/ecto/repo/queryable.ex:232: Ecto.Repo.Queryable.execute/4
(ecto 3.11.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
(ecto 3.11.2) lib/ecto/repo/queryable.ex:154: Ecto.Repo.Queryable.one/3
>> (chatter 0.3.0) lib/chatter/account/session.ex:53: Chatter.Account.Session.get/3
(phoenix 1.7.11) lib/phoenix/socket/transport.ex:495: Phoenix.Socket.Transport.connect_session/3
(phoenix 1.7.11) lib/phoenix/socket/transport.ex:481: anonymous fn/3 in Phoenix.Socket.Transport.connect_info/3
(elixir 1.16.2) lib/enum.ex:1700: Enum."-map/2-lists^map/1-1-"/2
(elixir 1.16.2) lib/enum.ex:1700: Enum."-map/2-lists^map/1-1-"/2
(phoenix 1.7.11) lib/phoenix/socket/transport.ex:463: Phoenix.Socket.Transport.connect_info/3
(phoenix 1.7.11) lib/phoenix/transports/websocket.ex:48: Phoenix.Transports.WebSocket.call/2
(chatter 0.3.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.do_socket_dispatch/2
(chatter 0.3.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2
(chatter 0.3.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2
(bandit 1.3.0) lib/bandit/pipeline.ex:103: Bandit.Pipeline.call_plug/2
(bandit 1.3.0) lib/bandit/pipeline.ex:24: Bandit.Pipeline.run/6
(bandit 1.3.0) lib/bandit/http1/handler.ex:33: Bandit.HTTP1.Handler.handle_data/3
(bandit 1.3.0) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
(bandit 1.3.0) /home/dbaer/chatter/deps/thousand_island/lib/thousand_island/handler.ex:411: Bandit.DelegatingHandler.handle_continue/2
Last message: {:continue, :handle_connection}
which is this hairy hack job:
def get(_conn, sid, _opts) do
query =
from(s in Session,
where: s.key == ^decode(sid)
)
case Repo.one(query) do
%Session{} = session ->
if Timex.before?(Timex.now(), session.expiry) do
data = Map.put(session.data, "id", session.id)
{sid, data}
else
sid = put(nil, sid, %{}, nil)
get(nil, sid, nil)
end
nil ->
sid = put(nil, sid, %{}, nil)
get(nil, sid, nil)
end
end
Naturally everything works as expected when running without async
enabled.
I’ve tried a few things, including what I use in the production app:
setup do
:ok = Sandbox.checkout(App.Repo)
identity =
insert(:identity, roles: [build(:account_role, role: build(:system_role, name: "ROOT"))])
{:ok, session: build_authed_session(identity), identity: identity}
end
though I don’t use the pre-built users in the test bench. I’ve also tried some variations without Wallaby.Feature
, as suggested by José in previous issue threads:
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Chatter.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Chatter.Repo, self())
{:ok, session} = Wallaby.start_session(metadata: metadata)
{:ok, session: session}
end
Somehow the session store is operating outside of the connection process where everything else works, though I’m not sure how that’s possible.
Are there any further avenues to probe? I’m investigating using Wallaby in said production app, but I fear the tests will take an untenable amount of time to run in non-async mode.
Thank you!