defmodule Ecto.Integration.SandboxTest do use ExUnit.Case alias Ecto.Adapters.SQL.Sandbox alias Ecto.Integration.{PoolRepo, TestRepo} alias Ecto.Integration.Post import ExUnit.CaptureLog describe "errors" do test "raises if repo is not started or not exist" do assert_raise RuntimeError, ~r"could not lookup Ecto repo UnknownRepo because it was not started", fn -> Sandbox.mode(UnknownRepo, :manual) end end test "raises if repo is not using sandbox" do assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn -> Sandbox.mode(PoolRepo, :manual) end assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn -> Sandbox.checkout(PoolRepo) end end test "includes link to SQL sandbox on ownership errors" do assert_raise DBConnection.OwnershipError, ~r"See Ecto.Adapters.SQL.Sandbox docs for more information.", fn -> TestRepo.all(Post) end end end describe "mode" do test "uses the repository when checked out" do assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> TestRepo.all(Post) end Sandbox.checkout(TestRepo) assert TestRepo.all(Post) == [] Sandbox.checkin(TestRepo) assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> TestRepo.all(Post) end end test "uses the repository when allowed from another process" do assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> TestRepo.all(Post) end parent = self() Task.start_link fn -> Sandbox.checkout(TestRepo) Sandbox.allow(TestRepo, self(), parent) send parent, :allowed Process.sleep(:infinity) end assert_receive :allowed assert TestRepo.all(Post) == [] end test "uses the repository when shared from another process" do Sandbox.checkout(TestRepo) Sandbox.mode(TestRepo, {:shared, self()}) assert Task.async(fn -> TestRepo.all(Post) end) |> Task.await == [] after Sandbox.mode(TestRepo, :manual) end end describe "savepoints" do test "runs inside a sandbox that is rolled back on checkin" do Sandbox.checkout(TestRepo) assert TestRepo.insert(%Post{}) assert TestRepo.all(Post) != [] Sandbox.checkin(TestRepo) Sandbox.checkout(TestRepo) assert TestRepo.all(Post) == [] Sandbox.checkin(TestRepo) end test "runs inside a sandbox that may be disabled" do Sandbox.checkout(TestRepo, sandbox: false) assert TestRepo.insert(%Post{}) assert TestRepo.all(Post) != [] Sandbox.checkin(TestRepo) Sandbox.checkout(TestRepo) assert {1, _} = TestRepo.delete_all(Post) Sandbox.checkin(TestRepo) Sandbox.checkout(TestRepo, sandbox: false) assert {1, _} = TestRepo.delete_all(Post) Sandbox.checkin(TestRepo) end test "runs inside a sandbox with caller data when preloading associations" do Sandbox.checkout(TestRepo) assert TestRepo.insert(%Post{}) parent = self() Task.start_link fn -> Sandbox.allow(TestRepo, parent, self()) assert [_] = TestRepo.all(Post) |> TestRepo.preload([:author, :comments]) send parent, :success end assert_receive :success end test "runs inside a sidebox with custom ownership timeout" do :ok = Sandbox.checkout(TestRepo, ownership_timeout: 200) parent = self() assert capture_log(fn -> {:ok, pid} = Task.start(fn -> Sandbox.allow(TestRepo, parent, self()) TestRepo.transaction(fn -> Process.sleep(500) end) end) ref = Process.monitor(pid) assert_receive {:DOWN, ^ref, _, ^pid, _}, 1000 end) =~ "it owned the connection for longer than 200ms" end test "does not taint the sandbox on query errors" do Sandbox.checkout(TestRepo) {:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true) {:error, _} = TestRepo.query("INVALID") {:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true) Sandbox.checkin(TestRepo) end end describe "transactions" do @tag :transaction_isolation test "with custom isolation level" do Sandbox.checkout(TestRepo, isolation: "READ UNCOMMITTED") # Setting it to the same level later on works TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") # Even inside a transaction TestRepo.transaction fn -> TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") end end test "disconnects on transaction timeouts" do Sandbox.checkout(TestRepo) assert capture_log(fn -> {:error, :rollback} = TestRepo.transaction(fn -> Process.sleep(1000) end, timeout: 100) end) =~ "timed out" Sandbox.checkin(TestRepo) end end describe "checkouts" do test "with transaction inside checkout" do Sandbox.checkout(TestRepo) TestRepo.checkout(fn -> refute TestRepo.in_transaction? TestRepo.transaction(fn -> assert TestRepo.in_transaction? end) refute TestRepo.in_transaction? end) end test "with checkout inside transaction" do Sandbox.checkout(TestRepo) TestRepo.transaction(fn -> assert TestRepo.in_transaction? TestRepo.checkout(fn -> assert TestRepo.in_transaction? end) assert TestRepo.in_transaction? end) end end end