diff --git a/.formatter.exs b/.formatter.exs index 16d5948a..d7230de2 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,9 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,test}/**/*.{ex,exs}" + ], line_length: 80, import_deps: [:ecto, :stream_data] ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 986bde21..3230f1b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: - name: Compile run: mix compile --warnings-as-errors - name: Run Tests - run: mix coveralls.json --warnings-as-errors + run: mix coveralls.json.all --warnings-as-errors - uses: codecov/codecov-action@v4 with: files: ./cover/excoveralls.json @@ -112,8 +112,10 @@ jobs: mix local.rebar --force mix local.hex --force mix deps.get - - name: Run Tests + - name: Run Base Tests run: mix test + - name: Run Postgres Tests + run: mix test.postgres matrix-results: if: ${{ always() }} diff --git a/.gitignore b/.gitignore index c1619158..a8311262 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ flop-*.tar # Ignore dialyzer PLT .plts + +# Test SQLite DB lives here +/tmp/ \ No newline at end of file diff --git a/config/test.exs b/config/test.exs index d9917faf..7941cf14 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,13 +4,6 @@ config :flop, ecto_repos: [Flop.Repo], repo: Flop.Repo -config :flop, Flop.Repo, - username: "postgres", - password: "postgres", - database: "flop_test#{System.get_env("MIX_TEST_PARTITION")}", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox - config :stream_data, max_runs: if(System.get_env("CI"), do: 100, else: 50), max_run_time: if(System.get_env("CI"), do: 3000, else: 200) diff --git a/docker-compose.yml b/docker-compose.yml index 70577558..7f425dfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.7" services: postgres: image: postgres:12-alpine diff --git a/mix.exs b/mix.exs index 542e5d9e..5e04e4ec 100644 --- a/mix.exs +++ b/mix.exs @@ -3,6 +3,7 @@ defmodule Flop.MixProject do @source_url "https://github.com/woylie/flop" @version "0.26.1" + @adapters ~w(postgres sqlite) def project do [ @@ -13,16 +14,21 @@ defmodule Flop.MixProject do elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), test_coverage: [tool: ExCoveralls], + test_paths: test_paths(System.get_env("ECTO_ADAPTER")), preferred_cli_env: [ "coveralls.detail": :test, "coveralls.github": :test, "coveralls.html": :test, + "coveralls.html.all": :test, "coveralls.json": :test, + "coveralls.json.all": :test, "coveralls.post": :test, "ecto.create": :test, "ecto.drop": :test, "ecto.migrate": :test, "ecto.reset": :test, + "test.all": :test, + "test.adapters": :test, coveralls: :test, dialyzer: :test ], @@ -65,6 +71,7 @@ defmodule Flop.MixProject do {:excoveralls, "~> 0.10", only: :test}, {:nimble_options, "~> 1.0"}, {:postgrex, ">= 0.0.0", only: :test}, + {:ecto_sqlite3, "~> 0.17.0", only: :test}, {:stream_data, "~> 1.0", only: [:dev, :test]} ] end @@ -104,8 +111,57 @@ defmodule Flop.MixProject do defp aliases do [ - "ecto.reset": ["ecto.drop", "ecto.create --quiet", "ecto.migrate"], - test: ["ecto.create --quiet", "ecto.migrate", "test"] + "test.all": ["test", "test.adapters"], + "test.postgres": &test_adapters(["postgres"], &1), + "test.sqlite": &test_adapters(["sqlite"], &1), + "test.adapters": &test_adapters/1, + "coveralls.html.all": [ + "test.adapters --cover", + "coveralls.html --import-cover cover" + ], + "coveralls.json.all": [ + # only run postgres and base tests for coverage until sqlite tests are + # fixed + fn _ -> test_adapters(["postgres"], ["--cover"]) end, + "coveralls.json --import-cover cover" + ] ] end + + defp test_paths(adapter) when adapter in @adapters, + do: ["test/adapters/ecto/#{adapter}"] + + defp test_paths(nil), do: ["test/base"] + + defp test_paths(adapter) do + raise """ + unknown Ecto adapter + + Expected ECTO_ADAPTER to be one of: #{inspect(@adapters)} + + Got: #{inspect(adapter)} + """ + end + + defp test_adapters(adapters \\ @adapters, args) do + for adapter <- adapters do + IO.puts("==> Running tests for ECTO_ADAPTER=#{adapter} mix test") + + {_, res} = + System.cmd( + "mix", + ["test", ansi_option(), "--export-coverage=#{adapter}" | args], + into: IO.binstream(:stdio, :line), + env: [{"ECTO_ADAPTER", adapter}] + ) + + if res > 0 do + System.at_exit(fn _ -> exit({:shutdown, 1}) end) + end + end + end + + defp ansi_option do + if IO.ANSI.enabled?(), do: "--color", else: "--no-color" + end end diff --git a/mix.lock b/mix.lock index 0a32bc54..ef439626 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -7,10 +8,13 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.2", "200226e057f76c40be55fbac77771eb1a233260ab8ec7283f5da6d9402bde8de", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "a3838919c5a34c268c28cafab87b910bcda354a9a4e778658da46c149bb2c1da"}, + "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, + "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, diff --git a/priv/plts/.keep b/priv/plts/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs deleted file mode 100644 index 3a510b28..00000000 --- a/priv/repo/migrations/.formatter.exs +++ /dev/null @@ -1,5 +0,0 @@ -[ - import_deps: [:ecto_sql], - inputs: ["*.exs"], - line_length: 80 -] diff --git a/priv/repo/migrations/20200527145236_create_test_tables.exs b/priv/repo/migrations/20200527145236_create_test_tables.exs deleted file mode 100644 index 9c4940e8..00000000 --- a/priv/repo/migrations/20200527145236_create_test_tables.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Flop.Repo.Migrations.CreateTestTables do - use Ecto.Migration - - def change do - create table(:owners) do - add :age, :integer - add :email, :string - add :name, :string - add :tags, {:array, :string} - add :attributes, :map - add :extra, {:map, :string} - end - - create table(:pets) do - add :age, :integer - add :family_name, :string - add :given_name, :string - add :name, :string - add :owner_id, references(:owners) - add :species, :string - add :mood, :string - add :tags, {:array, :string} - end - - create table(:fruits) do - add :family, :string - add :name, :string - add :attributes, :map - add :extra, {:map, :string} - add :owner_id, references(:owners) - end - end -end diff --git a/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs b/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs deleted file mode 100644 index d5dee232..00000000 --- a/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Flop.Repo.Migrations.CreatePetsInSecondSchema do - use Ecto.Migration - - def change do - execute("CREATE SCHEMA other_schema;", "DROP SCHEMA other_schema;") - - create table(:pets, prefix: "other_schema") do - add :age, :integer - add :family_name, :string - add :given_name, :string - add :name, :string - add :owner_id, :integer - add :species, :string - add :mood, :string - add :tags, {:array, :string} - end - end -end diff --git a/priv/repo/migrations/20230614113912_composite_type.exs b/priv/repo/migrations/20230614113912_composite_type.exs deleted file mode 100644 index 564110a3..00000000 --- a/priv/repo/migrations/20230614113912_composite_type.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Sibill.Repo.Migrations.CompositeType do - use Ecto.Migration - - def up do - execute("CREATE TYPE public.distance AS (unit varchar, value float);") - end - - def down do - execute("DROP TYPE public.distance;") - end -end diff --git a/priv/repo/migrations/20230614114123_create_distance_table.exs b/priv/repo/migrations/20230614114123_create_distance_table.exs deleted file mode 100644 index 25296fe9..00000000 --- a/priv/repo/migrations/20230614114123_create_distance_table.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Sibill.Repo.Migrations.CreateDistanceTable do - use Ecto.Migration - - def change do - create table(:walking_distances) do - add(:trip, :distance) - end - end -end diff --git a/test/flop_test.exs b/test/adapters/ecto/cases/flop_test.exs similarity index 85% rename from test/flop_test.exs rename to test/adapters/ecto/cases/flop_test.exs index 701ebe89..97154d24 100644 --- a/test/flop_test.exs +++ b/test/adapters/ecto/cases/flop_test.exs @@ -1,5 +1,7 @@ -defmodule FlopTest do - use ExUnit.Case, async: true +defmodule Flop.Adapters.Ecto.FlopTest do + use Flop.Integration.Case, + async: Application.compile_env(:flop, :async_integration_tests, true) + use ExUnitProperties doctest Flop, import: true @@ -10,22 +12,15 @@ defmodule FlopTest do import Flop.TestUtil alias __MODULE__.TestProvider - alias Ecto.Adapters.SQL.Sandbox alias Ecto.Query.QueryExpr alias Flop.Filter alias Flop.Meta alias Flop.Repo - alias MyApp.Fruit alias MyApp.Owner alias MyApp.Pet - alias MyApp.Vegetable @pet_count_range 1..200 - setup do - :ok = Sandbox.checkout(Repo) - end - defmodule TestProvider do use Flop, repo: Flop.Repo, default_limit: 35 end @@ -264,6 +259,16 @@ defmodule FlopTest do end end + # test "applies empty filter" do + # require Flop.Adapter.Ecto.Operators + + # field = :species + + # d1 = dynamic([r], is_nil(field(r, ^field)) == ^true); d2 = dynamic([r], Flop.Adapter.Ecto.Operators.empty(:other) == ^true) + + # assert where(Pet, ^d1) == where(Pet, ^d2) + # end + test "applies empty and not_empty filter with string values" do check all pet_count <- integer(@pet_count_range), pets = @@ -744,6 +749,7 @@ defmodule FlopTest do assert Enum.map(Flop.all(Pet, flop), & &1.name) == [name_1, name_2] end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") @@ -767,6 +773,7 @@ defmodule FlopTest do assert Flop.count(Pet, flop) == 6 end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") @@ -980,6 +987,7 @@ defmodule FlopTest do Flop.meta(Pet, %Flop{page_size: 3, page: 2}) end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") @@ -1717,272 +1725,6 @@ defmodule FlopTest do end end - describe "validate/1" do - test "returns Flop struct" do - assert Flop.validate(%Flop{}) == {:ok, %Flop{limit: 50}} - assert Flop.validate(%{}) == {:ok, %Flop{limit: 50}} - end - - test "returns error if parameters are invalid" do - assert {:error, %Meta{} = meta} = - Flop.validate( - %{ - limit: -1, - filters: [%{field: :name}, %{field: :age, op: "approx"}] - }, - for: Pet - ) - - assert meta.flop == %Flop{} - assert meta.schema == Pet - - assert meta.params == %{ - "limit" => -1, - "filters" => [ - %{"field" => :name}, - %{"field" => :age, "op" => "approx"} - ] - } - - assert [{"must be greater than %{number}", _}] = - Keyword.get(meta.errors, :limit) - - assert [[], [op: [{"is invalid", _}]]] = - Keyword.get(meta.errors, :filters) - end - - test "returns error if operator is not allowed for field" do - assert {:error, %Meta{} = meta} = - Flop.validate( - %{filters: [%{field: :age, op: "=~", value: 20}]}, - for: Pet - ) - - assert meta.flop == %Flop{} - assert meta.schema == Pet - - assert meta.params == %{ - "filters" => [%{"field" => :age, "op" => "=~", "value" => 20}] - } - - assert [ - [ - op: [ - {"is invalid", - [ - allowed_operators: [ - :==, - :!=, - :empty, - :not_empty, - :<=, - :<, - :>=, - :>, - :in, - :not_in - ] - ]} - ] - ] - ] = Keyword.get(meta.errors, :filters) - end - - test "returns filter params as list if passed as a map" do - assert {:error, %Meta{} = meta} = - Flop.validate( - %{ - limit: -1, - filters: %{ - "0" => %{field: :name}, - "1" => %{field: :age, op: "approx"} - } - }, - for: Pet - ) - - assert meta.params == %{ - "limit" => -1, - "filters" => [ - %{"field" => :name}, - %{"field" => :age, "op" => "approx"} - ] - } - end - end - - describe "validate!/1" do - test "returns a flop struct" do - assert Flop.validate!(%Flop{}) == %Flop{limit: 50} - assert Flop.validate!(%{}) == %Flop{limit: 50} - end - - test "raises if params are invalid" do - error = - assert_raise Flop.InvalidParamsError, fn -> - Flop.validate!(%{ - limit: -1, - filters: [%{field: :name}, %{field: :age, op: "approx"}] - }) - end - - assert error.params == - %{ - "limit" => -1, - "filters" => [ - %{"field" => :name}, - %{"field" => :age, "op" => "approx"} - ] - } - - assert [{"must be greater than %{number}", _}] = - Keyword.get(error.errors, :limit) - - assert [[], [op: [{"is invalid", _}]]] = - Keyword.get(error.errors, :filters) - end - end - - describe "named_bindings/3" do - test "returns used binding names with order_by and filters" do - flop = %Flop{ - filters: [ - # join fields - %Flop.Filter{field: :owner_age, op: :==, value: 5}, - %Flop.Filter{field: :owner_name, op: :==, value: "George"}, - # compound field - %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} - ], - # join field and normal field - order_by: [:owner_name, :age] - } - - assert Flop.named_bindings(flop, Pet) == [:owner] - end - - test "allows disabling order fields" do - flop = %Flop{order_by: [:owner_name, :age]} - assert Flop.named_bindings(flop, Pet, order: false) == [] - assert Flop.named_bindings(flop, Pet, order: true) == [:owner] - end - - test "returns used binding names with order_by" do - flop = %Flop{ - # join field and normal field - order_by: [:owner_name, :age] - } - - assert Flop.named_bindings(flop, Pet) == [:owner] - end - - test "returns used binding names with filters" do - flop = %Flop{ - filters: [ - # join fields - %Flop.Filter{field: :owner_age, op: :==, value: 5}, - %Flop.Filter{field: :owner_name, op: :==, value: "George"}, - # compound field - %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} - ] - } - - assert Flop.named_bindings(flop, Pet) == [:owner] - end - - test "returns used binding names with custom filter using bindings opt" do - flop = %Flop{ - filters: [ - %Flop.Filter{field: :with_bindings, op: :==, value: 5} - ] - } - - assert Flop.named_bindings(flop, Vegetable) == [:curious] - end - - test "returns empty list if no join fields are used" do - flop = %Flop{ - filters: [ - # compound field - %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} - ], - # normal field - order_by: [:age] - } - - assert Flop.named_bindings(flop, Pet) == [] - end - - test "returns empty list if there are no filters and order fields" do - assert Flop.named_bindings(%Flop{}, Pet) == [] - end - end - - describe "with_named_bindings/4" do - test "adds necessary bindings to query" do - query = Pet - opts = [for: Pet] - - flop = %Flop{ - filters: [ - # join fields - %Flop.Filter{field: :owner_age, op: :==, value: 5}, - %Flop.Filter{field: :owner_name, op: :==, value: "George"}, - # compound field - %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} - ], - # join field and normal field - order_by: [:owner_name, :age] - } - - fun = fn q, :owner -> - join(q, :left, [p], o in assoc(p, :owner), as: :owner) - end - - new_query = Flop.with_named_bindings(query, flop, fun, opts) - assert Ecto.Query.has_named_binding?(new_query, :owner) - end - - test "allows disabling order fields" do - query = Pet - flop = %Flop{order_by: [:owner_name, :age]} - - fun = fn q, :owner -> - join(q, :left, [p], o in assoc(p, :owner), as: :owner) - end - - opts = [for: Pet, order: false] - new_query = Flop.with_named_bindings(query, flop, fun, opts) - assert new_query == query - - opts = [for: Pet, order: true] - new_query = Flop.with_named_bindings(query, flop, fun, opts) - assert Ecto.Query.has_named_binding?(new_query, :owner) - end - - test "returns query unchanged if no bindings are required" do - query = Pet - opts = [for: Pet] - - assert Flop.with_named_bindings( - query, - %Flop{}, - fn _, _ -> nil end, - opts - ) == query - end - end - - describe "push_order/3" do - test "raises error if invalid directions option is passed" do - for flop <- [%Flop{}, %Flop{order_by: [:name], order_directions: [:asc]}], - directions <- [{:up, :down}, "up,down"] do - assert_raise Flop.InvalidDirectionsError, fn -> - Flop.push_order(flop, :name, directions: directions) - end - end - end - end - describe "__using__/1" do test "defines wrapper functions that pass default options" do insert_list(3, :pet) @@ -2028,56 +1770,4 @@ defmodule FlopTest do assert Keyword.get(opts, :backend) == TestProviderNested end end - - describe "get_option/3" do - test "returns value from option list" do - # sanity check - default_limit = Flop.Schema.default_limit(%Fruit{}) - assert default_limit && default_limit != 40 - - assert Flop.get_option( - :default_limit, - [default_limit: 40, backend: TestProvider, for: Fruit], - 1 - ) == 40 - end - - test "falls back to schema option" do - # sanity check - assert default_limit = Flop.Schema.default_limit(%Fruit{}) - - assert Flop.get_option( - :default_limit, - [backend: TestProvider, for: Fruit], - 1 - ) == default_limit - end - - test "falls back to backend config if schema option is not set" do - # sanity check - assert Flop.Schema.default_limit(%Pet{}) == nil - - assert Flop.get_option( - :default_limit, - [backend: TestProvider, for: Pet], - 1 - ) == 35 - end - - test "falls back to backend config if :for option is not set" do - assert Flop.get_option(:default_limit, [backend: TestProvider], 1) == 35 - end - - test "falls back to default value" do - assert Flop.get_option(:default_limit, []) == 50 - end - - test "falls back to default value passed to function" do - assert Flop.get_option(:some_option, [], 2) == 2 - end - - test "falls back to nil" do - assert Flop.get_option(:some_option, []) == nil - end - end end diff --git a/test/adapters/ecto/postgres/all_test.exs b/test/adapters/ecto/postgres/all_test.exs new file mode 100644 index 00000000..215fc306 --- /dev/null +++ b/test/adapters/ecto/postgres/all_test.exs @@ -0,0 +1 @@ +Code.require_file("../cases/flop_test.exs", __DIR__) diff --git a/test/adapters/ecto/postgres/migration.exs b/test/adapters/ecto/postgres/migration.exs new file mode 100644 index 00000000..7a9a25fc --- /dev/null +++ b/test/adapters/ecto/postgres/migration.exs @@ -0,0 +1,57 @@ +defmodule Flop.Repo.Postgres.Migration do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE public.distance AS (unit varchar, value float);", + "DROP TYPE public.distance;" + ) + + create table(:owners) do + add(:age, :integer) + add(:email, :string) + add(:name, :string) + add(:tags, {:array, :string}) + add(:attributes, :map) + add(:extra, {:map, :string}) + end + + create table(:pets) do + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, references(:owners)) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) + end + + create table(:fruits) do + add(:family, :string) + add(:name, :string) + add(:attributes, :map) + add(:extra, {:map, :string}) + add(:owner_id, references(:owners)) + end + + create table(:walking_distances) do + add(:trip, :distance) + end + + # create pets table in other schema + + execute("CREATE SCHEMA other_schema;", "DROP SCHEMA other_schema;") + + create table(:pets, prefix: "other_schema") do + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, :integer) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) + end + end +end diff --git a/test/adapters/ecto/postgres/test_helper.exs b/test/adapters/ecto/postgres/test_helper.exs new file mode 100644 index 00000000..d0d4f5e3 --- /dev/null +++ b/test/adapters/ecto/postgres/test_helper.exs @@ -0,0 +1,43 @@ +Application.put_env(:flop, :async_integration_tests, true) + +# Configure PG connection +Application.put_env(:flop, Flop.Repo, + username: "postgres", + password: "postgres", + database: "flop_test#{System.get_env("MIX_TEST_PARTITION")}", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox +) + +defmodule Flop.Repo do + use Ecto.Repo, + otp_app: :flop, + adapter: Ecto.Adapters.Postgres +end + +defmodule Flop.Integration.Case do + use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox + + setup do + :ok = Sandbox.checkout(Flop.Repo) + end +end + +Code.require_file("migration.exs", __DIR__) + +{:ok, _} = + Ecto.Adapters.Postgres.ensure_all_started(Flop.Repo.config(), :temporary) + +# Load up the repository, start it, and run migrations +Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) +Ecto.Adapters.Postgres.storage_up(Flop.Repo.config()) + +{:ok, _pid} = Flop.Repo.start_link() + +Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.Postgres.Migration, log: true) + +Ecto.Adapters.SQL.Sandbox.mode(Flop.Repo, :manual) + +{:ok, _} = Application.ensure_all_started(:ex_machina) +ExUnit.start() diff --git a/test/adapters/ecto/sqlite/all_test.exs b/test/adapters/ecto/sqlite/all_test.exs new file mode 100644 index 00000000..215fc306 --- /dev/null +++ b/test/adapters/ecto/sqlite/all_test.exs @@ -0,0 +1 @@ +Code.require_file("../cases/flop_test.exs", __DIR__) diff --git a/test/adapters/ecto/sqlite/migration.exs b/test/adapters/ecto/sqlite/migration.exs new file mode 100644 index 00000000..49d469e6 --- /dev/null +++ b/test/adapters/ecto/sqlite/migration.exs @@ -0,0 +1,37 @@ +defmodule Flop.Repo.SQLite.Migration do + use Ecto.Migration + + def change do + create table(:owners) do + add(:age, :integer) + add(:email, :string) + add(:name, :string) + add(:tags, {:array, :string}) + add(:attributes, :map) + add(:extra, {:map, :string}) + end + + create table(:pets) do + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, references(:owners)) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) + end + + create table(:fruits) do + add(:family, :string) + add(:name, :string) + add(:attributes, :map) + add(:extra, {:map, :string}) + add(:owner_id, references(:owners)) + end + + create table(:walking_distances) do + add(:trip, :distance) + end + end +end diff --git a/test/adapters/ecto/sqlite/test_helper.exs b/test/adapters/ecto/sqlite/test_helper.exs new file mode 100644 index 00000000..b17ca214 --- /dev/null +++ b/test/adapters/ecto/sqlite/test_helper.exs @@ -0,0 +1,39 @@ +Application.put_env(:flop, :async_integration_tests, false) + +# Configure SQLite db +Application.put_env(:flop, Flop.Repo, + database: "tmp/test.db", + pool: Ecto.Adapters.SQL.Sandbox, + show_sensitive_data_on_connection_error: true +) + +defmodule Flop.Repo do + use Ecto.Repo, + otp_app: :flop, + adapter: Ecto.Adapters.SQLite3 +end + +defmodule Flop.Integration.Case do + use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox + + setup do + :ok = Sandbox.checkout(Flop.Repo) + end +end + +Code.require_file("migration.exs", __DIR__) + +{:ok, _} = + Ecto.Adapters.SQLite3.ensure_all_started(Flop.Repo.config(), :temporary) + +# Load up the repository, start it, and run migrations +_ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) +:ok = Ecto.Adapters.SQLite3.storage_up(Flop.Repo.config()) + +{:ok, _pid} = Flop.Repo.start_link() + +:ok = Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.SQLite.Migration, log: false) + +{:ok, _} = Application.ensure_all_started(:ex_machina) +ExUnit.start(exclude: [:prefix]) diff --git a/test/flop/cursor_test.exs b/test/base/flop/cursor_test.exs similarity index 100% rename from test/flop/cursor_test.exs rename to test/base/flop/cursor_test.exs diff --git a/test/custom_types/any_test.exs b/test/base/flop/custom_types/any_test.exs similarity index 100% rename from test/custom_types/any_test.exs rename to test/base/flop/custom_types/any_test.exs diff --git a/test/custom_types/existing_atom_test.exs b/test/base/flop/custom_types/existing_atom_test.exs similarity index 100% rename from test/custom_types/existing_atom_test.exs rename to test/base/flop/custom_types/existing_atom_test.exs diff --git a/test/flop/filter_test.exs b/test/base/flop/filter_test.exs similarity index 100% rename from test/flop/filter_test.exs rename to test/base/flop/filter_test.exs diff --git a/test/flop/meta_test.exs b/test/base/flop/meta_test.exs similarity index 100% rename from test/flop/meta_test.exs rename to test/base/flop/meta_test.exs diff --git a/test/flop/misc_test.exs b/test/base/flop/misc_test.exs similarity index 100% rename from test/flop/misc_test.exs rename to test/base/flop/misc_test.exs diff --git a/test/flop/relay_test.exs b/test/base/flop/relay_test.exs similarity index 100% rename from test/flop/relay_test.exs rename to test/base/flop/relay_test.exs diff --git a/test/flop/schema_test.exs b/test/base/flop/schema_test.exs similarity index 100% rename from test/flop/schema_test.exs rename to test/base/flop/schema_test.exs diff --git a/test/flop/validation_test.exs b/test/base/flop/validation_test.exs similarity index 100% rename from test/flop/validation_test.exs rename to test/base/flop/validation_test.exs diff --git a/test/base/flop_test.exs b/test/base/flop_test.exs new file mode 100644 index 00000000..6edc124d --- /dev/null +++ b/test/base/flop_test.exs @@ -0,0 +1,333 @@ +defmodule FlopTest do + use ExUnit.Case, async: true + + import Ecto.Query + + alias __MODULE__.TestProvider + alias Flop.Meta + alias MyApp.Fruit + alias MyApp.Pet + alias MyApp.Vegetable + + defmodule TestProvider do + use Flop, repo: Flop.Repo, default_limit: 35 + end + + describe "validate/1" do + test "returns Flop struct" do + assert Flop.validate(%Flop{}) == {:ok, %Flop{limit: 50}} + assert Flop.validate(%{}) == {:ok, %Flop{limit: 50}} + end + + test "returns error if parameters are invalid" do + assert {:error, %Meta{} = meta} = + Flop.validate( + %{ + limit: -1, + filters: [%{field: :name}, %{field: :age, op: "approx"}] + }, + for: Pet + ) + + assert meta.flop == %Flop{} + assert meta.schema == Pet + + assert meta.params == %{ + "limit" => -1, + "filters" => [ + %{"field" => :name}, + %{"field" => :age, "op" => "approx"} + ] + } + + assert [{"must be greater than %{number}", _}] = + Keyword.get(meta.errors, :limit) + + assert [[], [op: [{"is invalid", _}]]] = + Keyword.get(meta.errors, :filters) + end + + test "returns error if operator is not allowed for field" do + assert {:error, %Meta{} = meta} = + Flop.validate( + %{filters: [%{field: :age, op: "=~", value: 20}]}, + for: Pet + ) + + assert meta.flop == %Flop{} + assert meta.schema == Pet + + assert meta.params == %{ + "filters" => [%{"field" => :age, "op" => "=~", "value" => 20}] + } + + assert [ + [ + op: [ + {"is invalid", + [ + allowed_operators: [ + :==, + :!=, + :empty, + :not_empty, + :<=, + :<, + :>=, + :>, + :in, + :not_in + ] + ]} + ] + ] + ] = Keyword.get(meta.errors, :filters) + end + + test "returns filter params as list if passed as a map" do + assert {:error, %Meta{} = meta} = + Flop.validate( + %{ + limit: -1, + filters: %{ + "0" => %{field: :name}, + "1" => %{field: :age, op: "approx"} + } + }, + for: Pet + ) + + assert meta.params == %{ + "limit" => -1, + "filters" => [ + %{"field" => :name}, + %{"field" => :age, "op" => "approx"} + ] + } + end + end + + describe "validate!/1" do + test "returns a flop struct" do + assert Flop.validate!(%Flop{}) == %Flop{limit: 50} + assert Flop.validate!(%{}) == %Flop{limit: 50} + end + + test "raises if params are invalid" do + error = + assert_raise Flop.InvalidParamsError, fn -> + Flop.validate!(%{ + limit: -1, + filters: [%{field: :name}, %{field: :age, op: "approx"}] + }) + end + + assert error.params == + %{ + "limit" => -1, + "filters" => [ + %{"field" => :name}, + %{"field" => :age, "op" => "approx"} + ] + } + + assert [{"must be greater than %{number}", _}] = + Keyword.get(error.errors, :limit) + + assert [[], [op: [{"is invalid", _}]]] = + Keyword.get(error.errors, :filters) + end + end + + describe "named_bindings/3" do + test "returns used binding names with order_by and filters" do + flop = %Flop{ + filters: [ + # join fields + %Flop.Filter{field: :owner_age, op: :==, value: 5}, + %Flop.Filter{field: :owner_name, op: :==, value: "George"}, + # compound field + %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} + ], + # join field and normal field + order_by: [:owner_name, :age] + } + + assert Flop.named_bindings(flop, Pet) == [:owner] + end + + test "allows disabling order fields" do + flop = %Flop{order_by: [:owner_name, :age]} + assert Flop.named_bindings(flop, Pet, order: false) == [] + assert Flop.named_bindings(flop, Pet, order: true) == [:owner] + end + + test "returns used binding names with order_by" do + flop = %Flop{ + # join field and normal field + order_by: [:owner_name, :age] + } + + assert Flop.named_bindings(flop, Pet) == [:owner] + end + + test "returns used binding names with filters" do + flop = %Flop{ + filters: [ + # join fields + %Flop.Filter{field: :owner_age, op: :==, value: 5}, + %Flop.Filter{field: :owner_name, op: :==, value: "George"}, + # compound field + %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} + ] + } + + assert Flop.named_bindings(flop, Pet) == [:owner] + end + + test "returns used binding names with custom filter using bindings opt" do + flop = %Flop{ + filters: [ + %Flop.Filter{field: :with_bindings, op: :==, value: 5} + ] + } + + assert Flop.named_bindings(flop, Vegetable) == [:curious] + end + + test "returns empty list if no join fields are used" do + flop = %Flop{ + filters: [ + # compound field + %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} + ], + # normal field + order_by: [:age] + } + + assert Flop.named_bindings(flop, Pet) == [] + end + + test "returns empty list if there are no filters and order fields" do + assert Flop.named_bindings(%Flop{}, Pet) == [] + end + end + + describe "with_named_bindings/4" do + test "adds necessary bindings to query" do + query = Pet + opts = [for: Pet] + + flop = %Flop{ + filters: [ + # join fields + %Flop.Filter{field: :owner_age, op: :==, value: 5}, + %Flop.Filter{field: :owner_name, op: :==, value: "George"}, + # compound field + %Flop.Filter{field: :full_name, op: :==, value: "George the Dog"} + ], + # join field and normal field + order_by: [:owner_name, :age] + } + + fun = fn q, :owner -> + join(q, :left, [p], o in assoc(p, :owner), as: :owner) + end + + new_query = Flop.with_named_bindings(query, flop, fun, opts) + assert Ecto.Query.has_named_binding?(new_query, :owner) + end + + test "allows disabling order fields" do + query = Pet + flop = %Flop{order_by: [:owner_name, :age]} + + fun = fn q, :owner -> + join(q, :left, [p], o in assoc(p, :owner), as: :owner) + end + + opts = [for: Pet, order: false] + new_query = Flop.with_named_bindings(query, flop, fun, opts) + assert new_query == query + + opts = [for: Pet, order: true] + new_query = Flop.with_named_bindings(query, flop, fun, opts) + assert Ecto.Query.has_named_binding?(new_query, :owner) + end + + test "returns query unchanged if no bindings are required" do + query = Pet + opts = [for: Pet] + + assert Flop.with_named_bindings( + query, + %Flop{}, + fn _, _ -> nil end, + opts + ) == query + end + end + + describe "push_order/3" do + test "raises error if invalid directions option is passed" do + for flop <- [%Flop{}, %Flop{order_by: [:name], order_directions: [:asc]}], + directions <- [{:up, :down}, "up,down"] do + assert_raise Flop.InvalidDirectionsError, fn -> + Flop.push_order(flop, :name, directions: directions) + end + end + end + end + + describe "get_option/3" do + test "returns value from option list" do + # sanity check + default_limit = Flop.Schema.default_limit(%Fruit{}) + assert default_limit && default_limit != 40 + + assert Flop.get_option( + :default_limit, + [default_limit: 40, backend: TestProvider, for: Fruit], + 1 + ) == 40 + end + + test "falls back to schema option" do + # sanity check + assert default_limit = Flop.Schema.default_limit(%Fruit{}) + + assert Flop.get_option( + :default_limit, + [backend: TestProvider, for: Fruit], + 1 + ) == default_limit + end + + test "falls back to backend config if schema option is not set" do + # sanity check + assert Flop.Schema.default_limit(%Pet{}) == nil + + assert Flop.get_option( + :default_limit, + [backend: TestProvider, for: Pet], + 1 + ) == 35 + end + + test "falls back to backend config if :for option is not set" do + assert Flop.get_option(:default_limit, [backend: TestProvider], 1) == 35 + end + + test "falls back to default value" do + assert Flop.get_option(:default_limit, []) == 50 + end + + test "falls back to default value passed to function" do + assert Flop.get_option(:some_option, [], 2) == 2 + end + + test "falls back to nil" do + assert Flop.get_option(:some_option, []) == nil + end + end +end diff --git a/test/base/test_helper.exs b/test/base/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/test/base/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test/support/repo.ex b/test/support/repo.ex deleted file mode 100644 index 4e6fb658..00000000 --- a/test/support/repo.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Flop.Repo do - use Ecto.Repo, - otp_app: :flop, - adapter: Ecto.Adapters.Postgres -end diff --git a/test/test_helper.exs b/test/test_helper.exs deleted file mode 100644 index 4897e3bb..00000000 --- a/test/test_helper.exs +++ /dev/null @@ -1,3 +0,0 @@ -{:ok, _pid} = Flop.Repo.start_link() -{:ok, _} = Application.ensure_all_started(:ex_machina) -ExUnit.start()