diff --git a/.github/workflows/elixir_tests.yml b/.github/workflows/elixir_tests.yml index b2af713..80899b2 100644 --- a/.github/workflows/elixir_tests.yml +++ b/.github/workflows/elixir_tests.yml @@ -68,6 +68,10 @@ jobs: ${{ runner.os }}-build-${{ env.MIX_ENV }}- ${{ runner.os }}-build- + # needed for the igniter tests + - name: Install phx.new task + run: mix archive.install hex phx_new --force + - name: Install dependencies run: mix deps.get && mix deps.compile diff --git a/README.md b/README.md index 708a8ce..d741df9 100644 --- a/README.md +++ b/README.md @@ -203,11 +203,44 @@ See the `Phoenix.Sync.Writer` module docs for more information. `Phoenix.Sync` can be used in two modes: 1. `:embedded` where Electric is included as an application dependency and Phoenix.Sync consumes data internally using Elixir APIs -2. `:http` where Electric does _not_ need to be included as an application dependency and Phoenix.Sync consumes data from an external Electric service using it's [HTTP API](https://electric-sql.com/docs/api/http) -### Embedded mode + In `:embedded` mode, Electric does not expose an HTTP API (internally or externally). Messages are streamed internally between Electric and Phoenix.Sync using Elixir function APIs. The only HTTP API for sync is that exposed via your Phoenix Router using the `sync/2` macro and `sync_render/3` function. -In `:embedded` mode, Electric must be included an application dependency but does not expose an HTTP API (internally or externally). Messages are streamed internally between Electric and Phoenix.Sync using Elixir function APIs. The only HTTP API for sync is that exposed via your Phoenix Router using the `sync/2` macro and `sync_render/3` function. +2. `:http` where Electric does _not_ need to be included as an application dependency and Phoenix.Sync consumes data from an external Electric service using it's [HTTP API](https://electric-sql.com/docs/api/http). As for `embedded` mode, the only HTTP API for sync is that exposed via `Phoenix.Sync.Router.sync/2` and `Phoenix.Sync.Controller.sync_render/3`. + +### Using Igniter + +`Phoenix.Sync` supports [`Igniter`](https://hexdocs.pm/igniter/readme.html) for automatic configuration of your application. + +Add Igniter to an existing Elixir project by adding it to your dependencies in mix.exs: + +```elixir +{:igniter, "~> 0.6", only: [:dev, :test]} +``` + +Then use it to install `Phoenix.Sync` in `embedded` mode: + +```elixir +mix igniter.install phoenix_sync --sync-mode embedded +``` + +This will install `Phoenix.Sync` into your application and also set up your `Ecto.Repo` with the [sandbox test adapter](`Phoenix.Sync.Sandbox`). + +If you don't want the sandbox support then pass `--no-sync-sandbox`: + +```elixir +mix igniter.install phoenix_sync --sync-mode embedded --no-sync-sandbox +``` + +For `http` mode: + +```elixir +mix igniter.install phoenix_sync --sync-mode http --sync-url "https://api.electric-sql.cloud" +``` + +### Manual installation + +#### Embedded mode Example config: @@ -234,9 +267,7 @@ children = [ ] ``` -### HTTP - -In `:http` mode, Electric does not need to be included as an application dependency. Instead, Phoenix.Sync consumes data from an external Electric service over HTTP. +#### HTTP mode ```elixir # mix.exs diff --git a/lib/mix/tasks/phoenix_sync.install.ex b/lib/mix/tasks/phoenix_sync.install.ex new file mode 100644 index 0000000..34e6214 --- /dev/null +++ b/lib/mix/tasks/phoenix_sync.install.ex @@ -0,0 +1,415 @@ +defmodule Mix.Tasks.PhoenixSync.Install.Docs do + @moduledoc false + + @spec short_doc() :: String.t() + def short_doc do + "Install Phoenix.Sync into an existing Phoenix or Plug application" + end + + @spec example() :: String.t() + def example do + "mix phoenix_sync.install --sync-mode embedded" + end + + @spec long_doc() :: String.t() + def long_doc do + """ + #{short_doc()} + + Usually invoked using `igniter.install`: + + ```sh + mix igniter.install phoenix_sync --sync-mode embedded + ``` + + But can be invoked directly if `:phoenix_sync` is already a dependency: + + ```sh + #{example()} + ``` + + ## Options + + * `--sync-mode` - How to connect to Electric, either `embedded` or `http`. + + - `embedded` - `:electric` will be added as a dependency and will connect to the database your repo is configured for. + - `http` - You'll need to specify the `--sync-url` to the remote Electric server. + + ### Options for `embedded` mode + + * `--no-sync-sandbox` - Disable the test sandbox + + ### Options for `http` mode + + * `--sync-url` (required) - The URL of the Electric server, required for `http` mode. + """ + end +end + +if Code.ensure_loaded?(Igniter) do + defmodule Mix.Tasks.PhoenixSync.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()}" + + @moduledoc __MODULE__.Docs.long_doc() + + use Igniter.Mix.Task + + require Igniter.Code.Function + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + group: :phoenix_sync, + adds_deps: [], + installs: [], + example: __MODULE__.Docs.example(), + only: nil, + positional: [], + composes: [], + schema: [ + sync_mode: :string, + sync_url: :string, + sync_sandbox: :boolean + ], + defaults: [], + aliases: [], + required: [:sync_mode] + } + end + + @valid_modes ~w[embedded http] + + @impl Igniter.Mix.Task + def igniter(igniter) do + {:ok, mode} = Keyword.fetch(igniter.args.options, :sync_mode) + + if mode not in @valid_modes do + Igniter.add_issue( + igniter, + "mode #{inspect(mode)} is invlid, valid modes are: #{@valid_modes |> Enum.join(", ")}" + ) + else + add_dependencies(igniter, mode) + end + end + + defp add_dependencies(igniter, "http") do + case Keyword.fetch(igniter.args.options, :sync_url) do + {:ok, url} -> + igniter + |> base_configuration(:http) + |> Igniter.Project.Config.configure_new( + "config.exs", + :phoenix_sync, + [:url], + url + ) + |> Igniter.Project.Config.configure_new( + "config.exs", + :phoenix_sync, + [:credentials], + secret: "MY_SECRET", + source_id: "00000000-0000-0000-0000-000000000000" + ) + |> configure_endpoint() + + :error -> + Igniter.add_issue(igniter, "`--sync-url` is required for :http mode") + end + end + + defp add_dependencies(igniter, "embedded") do + igniter + |> Igniter.Project.Deps.add_dep({:electric, required_electric_version()}, error?: true) + |> then(fn igniter -> + if igniter.assigns[:test_mode?] do + igniter + else + Igniter.apply_and_fetch_dependencies(igniter) + end + end) + |> base_configuration(:embedded) + |> find_repo() + |> configure_repo() + |> configure_endpoint() + end + + defp configure_endpoint(igniter) do + application = Igniter.Project.Application.app_module(igniter) + + case Igniter.Libs.Phoenix.select_endpoint(igniter) do + {igniter, nil} -> + configure_plug_app(igniter, application) + + {igniter, endpoint} -> + configure_phoenix_endpoint(igniter, application, endpoint) + end + end + + defp configure_plug_app(igniter, application) do + # find plug module in application children and add config there + set_plug_opts(igniter, application, [Plug.Cowboy, Bandit], fn zipper -> + with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do + Igniter.Code.Keyword.set_keyword_key( + zipper, + :plug, + nil, + fn zipper -> + if Igniter.Code.Tuple.tuple?(zipper) do + with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do + Igniter.Code.Keyword.set_keyword_key( + zipper, + :phoenix_sync, + quote(do: Phoenix.Sync.plug_opts()) + ) + end + else + with {:ok, plug} <- Igniter.Code.Common.expand_literal(zipper) do + {:ok, + zipper + |> Sourceror.Zipper.search_pattern("#{inspect(plug)}") + |> Igniter.Code.Common.replace_code( + "{#{inspect(plug)}, phoenix_sync: Phoenix.Sync.plug_opts()}" + )} + end + end + end + ) + end + end) + end + + defp configure_phoenix_endpoint(igniter, application, endpoint) do + set_plug_opts(igniter, application, [endpoint], fn zipper -> + # gets called with a zipper on the endpoint module in the list of children + + if Igniter.Code.Tuple.tuple?(zipper) do + with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do + Igniter.Code.Keyword.set_keyword_key( + zipper, + :phoenix_sync, + quote(do: Phoenix.Sync.plug_opts()) + ) + end + else + # the search pattern call results in replacing the module name with + # the configuration whilst preserving the preceding comments + {:ok, + zipper + |> Sourceror.Zipper.search_pattern("#{inspect(endpoint)}") + |> Igniter.Code.Common.replace_code( + "{#{inspect(endpoint)}, phoenix_sync: Phoenix.Sync.plug_opts()}" + )} + end + end) + end + + defp configure_repo(%{issues: [_ | _] = _issues} = igniter) do + igniter + end + + defp configure_repo(igniter) do + case igniter.assigns do + %{repo: repo} when is_atom(repo) -> + igniter = + igniter + |> Igniter.Project.Config.configure_new("config.exs", :phoenix_sync, [:repo], repo) + |> Igniter.Project.Config.configure_new("test.exs", :phoenix_sync, [:mode], :sandbox) + |> Igniter.Project.Config.configure_new( + "test.exs", + :phoenix_sync, + [:env], + {:code, quote(do: config_env())} + ) + + enable_sandbox? = Keyword.get(igniter.args.options, :sync_sandbox, true) + + if enable_sandbox? do + igniter + |> Igniter.Project.Module.find_and_update_module!( + repo, + fn zipper -> + with :error <- + Igniter.Code.Module.move_to_use(zipper, Phoenix.Sync.Sandbox.Postgres), + {:ok, zipper} <- + Igniter.Code.Function.move_to_function_call(zipper, :use, [2]), + {:ok, zipper} <- + Igniter.Code.Function.update_nth_argument(zipper, 1, fn zipper -> + adapter = quote(do: Phoenix.Sync.Sandbox.Postgres.adapter()) + + Igniter.Code.Keyword.set_keyword_key( + zipper, + :adapter, + adapter, + fn z -> {:ok, Igniter.Code.Common.replace_code(z, adapter)} end + ) + end), + {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, Ecto.Repo), + zipper <- + Igniter.Code.Common.add_code(zipper, "use Phoenix.Sync.Sandbox.Postgres", + placement: :before + ) do + {:ok, zipper} + end + end + ) + else + igniter + end + + _ -> + igniter + |> Igniter.add_notice("No Ecto.Repo found, adding example `connection_opts` to config") + |> Igniter.Project.Config.configure_new( + "config.exs", + :phoenix_sync, + [:connection_opts], + {:code, + Sourceror.parse_string!(""" + # add your real database connection details + [ + username: "your_username", + password: "your_password", + hostname: "localhost", + database: "your_database", + port: 5432, + # sslmode can be: :disable, :allow, :prefer or :require + sslmode: :prefer + ] + """)} + ) + end + end + + defp base_configuration(igniter, mode) do + igniter + |> Igniter.Project.Config.configure_new( + "config.exs", + :phoenix_sync, + [:mode], + mode + ) + |> Igniter.Project.Config.configure_new( + "config.exs", + :phoenix_sync, + [:env], + {:code, quote(do: config_env())} + ) + end + + defp required_electric_version do + Phoenix.Sync.MixProject.project() + |> Keyword.fetch!(:deps) + |> Enum.find(&match?({:electric, _, _}, &1)) + |> elem(1) + end + + defp find_repo(igniter) do + case Igniter.Libs.Ecto.select_repo(igniter) do + {igniter, nil} -> + Igniter.add_notice( + igniter, + """ + No Ecto.Repo found in application environment. + + To use `embedded` mode you must add `connection_opts` to your config, e.g. + + config :phoenix_sync, + env: config_env(), + mode: :embedded, + connection_opts: [ + username: "your_username", + password: "your_password", + hostname: "localhost", + database: "your_database", + port: 5432 + ] + """ + ) + + {igniter, repo} -> + Igniter.assign(igniter, :repo, repo) + end + end + + defp set_plug_opts(igniter, application, modify_modules, updater) + when is_list(modify_modules) do + Igniter.Project.Module.find_and_update_module!(igniter, application, fn zipper -> + with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :start, 2), + {:ok, zipper} <- + Igniter.Code.Function.move_to_function_call_in_current_scope( + zipper, + :=, + [2], + fn call -> + Igniter.Code.Function.argument_matches_pattern?( + call, + 0, + {:children, _, context} when is_atom(context) + ) && + Igniter.Code.Function.argument_matches_pattern?(call, 1, v when is_list(v)) + end + ), + {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1) do + case Igniter.Code.List.move_to_list_item(zipper, fn item -> + case extract_child_module(item) do + {:ok, child_module} -> + Enum.any?(modify_modules, fn modify_module -> + Igniter.Code.Common.nodes_equal?(child_module, modify_module) + end) + + :error -> + false + end + end) do + {:ok, zipper} -> + updater.(zipper) + + :error -> + {:warning, + """ + Could not find a suitable `children = [...]` assignment in the `start` function of the `#{inspect(application)}` module. + Please add `phoenix_sync: Phoenix.Sync.plug_opts()` to your Phoenix endpoint or Plug module configuration + """} + end + else + _ -> + {:warning, + """ + Could not find a `children = [...]` assignment in the `start` function of the `#{inspect(application)}` module. + Please add `phoenix_sync: Phoenix.Sync.plug_opts()` to your Phoenix endpoint or Plug module configuration + """} + end + end) + end + + defp extract_child_module(zipper) do + if Igniter.Code.Tuple.tuple?(zipper) do + with {:ok, elem} <- Igniter.Code.Tuple.tuple_elem(zipper, 0) do + {:ok, Igniter.Code.Common.expand_alias(elem)} + end + else + {:ok, Igniter.Code.Common.expand_alias(zipper)} + end + end + end +else + defmodule Mix.Tasks.PhoenixSync.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" + + @moduledoc __MODULE__.Docs.long_doc() + + use Mix.Task + + @impl Mix.Task + def run(_argv) do + Mix.shell().error(""" + The task 'phoenix_sync.install' requires igniter. Please install igniter and try again. + + For more information, see: https://hexdocs.pm/igniter/readme.html#installation + """) + + exit({:shutdown, 1}) + end + end +end diff --git a/mix.exs b/mix.exs index b1a1328..8b4aa3c 100644 --- a/mix.exs +++ b/mix.exs @@ -3,6 +3,7 @@ defmodule Phoenix.Sync.MixProject do # Remember to update the README when you change the version @version "0.5.1" + @electric_version "~> 1.1.2" def project do [ @@ -35,6 +36,8 @@ defmodule Phoenix.Sync.MixProject do [preferred_envs: ["test.all": :test, "test.apps": :test]] end + def electric_version, do: @electric_version + defp deps do [ {:nimble_options, "~> 1.1"}, @@ -42,8 +45,9 @@ defmodule Phoenix.Sync.MixProject do {:plug, "~> 1.0"}, {:jason, "~> 1.0"}, {:ecto_sql, "~> 3.10", optional: true}, - {:electric, "~> 1.1.2", optional: true}, - {:electric_client, "~> 0.7"} + {:electric, @electric_version, optional: true}, + {:electric_client, "~> 0.7"}, + {:igniter, "~> 0.6", optional: true} ] ++ deps_for_env(Mix.env()) ++ json_deps() end diff --git a/mix.lock b/mix.lock index b839875..7d49bb2 100644 --- a/mix.lock +++ b/mix.lock @@ -24,10 +24,12 @@ "fine": {:hex, :fine, "0.1.2", "85cf7dd190c7c6c54c2840754ae977c9acc0417316255b674fad9f2678e4ecc7", [:mix], [], "hexpm", "9113531982c2b60dbea6c7233917ddf16806947cd7104b5d03011bf436ca3072"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.6.27", "a7c01062db56f5c5ac0f36ff8ef3cce1d61cd6bf59e50c52f4a38dc926aa9728", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "d1eda5271932dcb9f00f94936c3dc12a2b96466f895f4b3fb82a0caada6d6447"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lazy_html": {:hex, :lazy_html, "0.1.3", "8b9c8c135e95f7bc483de6195c4e1c0b2c913a5e2c57353ef4e82703b7ac8bd1", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "5f96f29587dcfed8a22281e8c44c6607e958ba821d90b9dfc003d1ef610f7d07"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -48,6 +50,7 @@ "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"}, "otel_metric_exporter": {:hex, :otel_metric_exporter, "0.2.5", "8b9e9253c85202ac47f4c1c16c4496e093b12d4afe11292c7e58a03e943e24c0", [:mix], [{:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.13.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "66f697a4aff251ba50a58564eb11efeed61aacbae12f151ba9a80bd87c92323e"}, "pg_query_ex": {:hex, :pg_query_ex, "0.9.0", "8e34bd2d0e0eb9e8d621c4697032fad4bfba46826950d3b46904a80ab589b43a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:protox, "~> 2.0", [hex: :protox, repo: "hexpm", optional: false]}], "hexpm", "a3fada1704fa9e2bc11ff846ad545ef9a1d34f46d86206063c37128960f4f5f5"}, + "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"}, "phoenix": {:hex, :phoenix, "1.8.0", "dc5d256bb253110266ded8c4a6a167e24fabde2e14b8e474d262840ae8d8ea18", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "15f6e9cb76646ad8d9f2947240519666fc2c4f29f8a93ad9c7664916ab4c167b"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.4", "619f80f7bc8e99d67bda9fafa28b0e3e0d69fad9fcbe859be8988bf9183d83b3", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad32d316c546b029ef22895355f9980c74657026ab5e093157d8990f2aa7bd5d"}, @@ -61,7 +64,10 @@ "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, "retry": {:hex, :retry, "0.19.0", "aeb326d87f62295d950f41e1255fe6f43280a1b390d36e280b7c9b00601ccbc2", [:mix], [], "hexpm", "85ef376aa60007e7bff565c366310966ec1bd38078765a0e7f20ec8a220d02ca"}, + "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, "sentry": {:hex, :sentry, "10.8.1", "aa45309785e1521416225adb16e0b4d8b957578804527f3c7babb6fefbc5e456", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "495b3cdadad90ba72eef973aa3dec39b3b8b2a362fe87e2f4ef32133ac3b4097"}, + "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, + "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_split": {:hex, :stream_split, "0.1.7", "2d3fd1fd21697da7f91926768d65f79409086052c9ec7ae593987388f52425f8", [:mix], [], "hexpm", "1dc072ff507a64404a0ad7af90df97096183fee8eeac7b300320cea7c4679147"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, @@ -69,6 +75,7 @@ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_metrics_statsd": {:hex, :telemetry_metrics_statsd, "0.7.1", "3502235bb5b35ce50d608bf0f34369ef76eb92a4dbc8708c7e8780ca0da2d53e", [:mix], [{:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "06338d9dc3b4a202f11a6e706fd3feba4c46100d0aca23688dea0b8f801c361f"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, "tls_certificate_check": {:hex, :tls_certificate_check, "1.29.0", "4473005eb0bbdad215d7083a230e2e076f538d9ea472c8009fd22006a4cfc5f6", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "5b0d0e5cb0f928bc4f210df667304ed91c5bff2a391ce6bdedfbfe70a8f096c5"}, "tz": {:hex, :tz, "0.28.1", "717f5ffddfd1e475e2a233e221dc0b4b76c35c4b3650b060c8e3ba29dd6632e9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "bfdca1aa1902643c6c43b77c1fb0cb3d744fd2f09a8a98405468afdee0848c8a"}, diff --git a/test/mix/tasks/phoenix_sync.install_test.exs b/test/mix/tasks/phoenix_sync.install_test.exs new file mode 100644 index 0000000..e5b4ea2 --- /dev/null +++ b/test/mix/tasks/phoenix_sync.install_test.exs @@ -0,0 +1,635 @@ +defmodule Mix.Tasks.PhoenixSync.InstallTest do + use ExUnit.Case, async: true + + import Igniter.Test + + defp run_install_task(igniter, ctx) do + install_args = Map.fetch!(ctx, :install_args) + files = Map.get(ctx, :files, %{}) + + # put the files after the base install -- needed otherwise the phx install + # task overwrites any sources we define here that conflict + igniter + |> Map.update!(:assigns, fn assigns -> + Map.update!(assigns, :test_files, &Map.merge(&1, Map.new(files))) + end) + |> apply_igniter!() + |> Igniter.compose_task("phoenix_sync.install", install_args) + end + + describe "phoenix: embedded" do + @describetag install_args: ["--sync-mode", "embedded"] + + setup(ctx) do + [igniter: run_install_task(phx_test_project(), ctx)] + end + + test "adds electric dependency", ctx do + ctx.igniter + |> assert_has_patch( + "mix.exs", + """ + + | {:electric, "#{Phoenix.Sync.MixProject.electric_version()}"} + """ + ) + end + + test "updates existing config files", ctx do + ctx.igniter + |> assert_has_patch( + "config/config.exs", + """ + + |config :phoenix_sync, mode: :embedded, env: config_env(), repo: Test.Repo + """ + ) + |> assert_has_patch( + "config/test.exs", + """ + + |config :phoenix_sync, mode: :sandbox, env: config_env() + """ + ) + end + + test "configures the endpoint", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test/application.ex", + """ + - | TestWeb.Endpoint + + | {TestWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} + """ + ) + end + + test "appends sync config to any existing params", ctx do + phx_test_project() + |> Igniter.update_file("lib/test/application.ex", fn source -> + Rewrite.Source.update(source, :content, fn content -> + String.replace( + content, + ~r/TestWeb\.Endpoint/, + "{TestWeb.Endpoint, config: [here: true]}" + ) + end) + end) + |> run_install_task(ctx) + |> assert_has_patch( + "lib/test/application.ex", + # the final patch is my source update above plus the effect of the install + """ + - | {TestWeb.Endpoint, config: [here: true]} + + | {TestWeb.Endpoint, config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()} + """ + ) + end + + test "adds the sandbox config to the repo", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test/repo.ex", + """ + + | use Phoenix.Sync.Sandbox.Postgres + + | + """ + ) + |> assert_has_patch( + "lib/test/repo.ex", + """ + - | adapter: Ecto.Adapters.Postgres + + | adapter: Phoenix.Sync.Sandbox.Postgres.adapter() + """ + ) + end + + @tag files: %{ + "lib/test/repo.ex" => """ + defmodule Test.Repo do + use Phoenix.Sync.Sandbox.Postgres + + use Ecto.Repo, + otp_app: :test, + adapter: Phoenix.Sync.Sandbox.Postgres.adapter() + end + """ + } + test "doesn't add the sandbox adapter if its already present", ctx do + ctx.igniter + |> assert_unchanged("lib/test/repo.ex") + end + + @tag install_args: ["--sync-mode", "embedded", "--no-sync-sandbox"] + test "doesn't add the sandbox config if passed --no-sync-sandbox", ctx do + ctx.igniter + |> assert_unchanged("lib/test/repo.ex") + end + + test "adds connection_opts to repo-less application", ctx do + phx_test_project() + |> Igniter.rm("lib/test/repo.ex") + |> run_install_task(ctx) + |> assert_has_notice(fn notice -> + assert notice =~ "No Ecto.Repo found" + end) + |> assert_has_patch( + "config/config.exs", + """ + + |# add your real database connection details + + |config :phoenix_sync, + + | mode: :embedded, + + | env: config_env(), + + | connection_opts: [ + + | username: "your_username", + + | password: "your_password", + + | hostname: "localhost", + + | database: "your_database", + + | port: 5432, + + | # sslmode can be: :disable, :allow, :prefer or :require + + | sslmode: :prefer + + | ] + """ + ) + end + end + + describe "phoenix: http" do + @describetag install_args: [ + "--sync-mode", + "http", + "--sync-url", + "https://electric-sql.cloud/" + ] + + setup(ctx) do + files = Map.get(ctx, :files, %{}) + + [igniter: run_install_task(phx_test_project(files: files), ctx)] + end + + @tag install_args: ["--sync-mode", "http"] + test "returns issue if url is not provided", ctx do + ctx.igniter + |> assert_has_issue(fn issue -> + assert issue =~ "`--sync-url` is required for :http mode" + end) + end + + test "adds url and empty credentials to config.exs", ctx do + ctx.igniter + |> assert_has_patch( + "config/config.exs", + """ + + |config :phoenix_sync, + + | mode: :http, + + | env: config_env(), + + | url: "https://electric-sql.cloud/", + + | credentials: [secret: "MY_SECRET", source_id: "00000000-0000-0000-0000-000000000000"] + + | + """ + ) + end + + test "configures the endpoint", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test/application.ex", + """ + - | TestWeb.Endpoint + + | {TestWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} + """ + ) + end + end + + plug_application = fn server_spec -> + """ + defmodule TestPlug.Application do + use Application + + def start(_type, _args) do + children = [ + #{server_spec} + ] + + opts = [strategy: :one_for_one, name: TestPlug.Supervisor] + Supervisor.start_link(children, opts) + end + end + """ + end + + @plug_files %{ + "mix.exs" => """ + defmodule TestPlug.MixProject do + use Mix.Project + + def project do + [ + app: :test_plug, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {TestPlug.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [] + end + end + """, + "lib/test_plug/application.ex" => + plug_application.( + "{Plug.Cowboy, scheme: :http, plug: PlugTest.Router, options: [port: 4040]}" + ), + "lib/test_plug/repo.ex" => """ + defmodule TestPlug.Repo do + use Ecto.Repo, + otp_app: :test_plug, + adapter: Ecto.Adapters.Postgres + end + """ + } + describe "Plug: embedded" do + @describetag install_args: ["--sync-mode", "embedded"] + + setup(ctx) do + extra_files = Map.get(ctx, :files, %{}) + + files = Map.merge(@plug_files, extra_files) + + [igniter: run_install_task(test_project(files: files), ctx)] + end + + test "adds electric dependency", ctx do + ctx.igniter + |> assert_has_patch( + "mix.exs", + """ + - | [] + + | [{:electric, "#{Phoenix.Sync.MixProject.electric_version()}"}] + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Plug.Cowboy, scheme: :http, plug: TestPlug.Router, options: [port: 4040]}" + ) + } + test "configures Plug.Cowboy app", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Plug.Cowboy, scheme: :http, plug: TestPlug.Router, options: [port: 4040]} + + | {Plug.Cowboy, + + | scheme: :http, + + | plug: {TestPlug.Router, phoenix_sync: Phoenix.Sync.plug_opts()}, + + | options: [port: 4040]} + + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Plug.Cowboy, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, options: [port: 4040]}" + ) + } + test "configures Plug.Cowboy app which already has config", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Plug.Cowboy, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, options: [port: 4040]} + + | {Plug.Cowboy, + + | scheme: :http, + + | plug: {TestPlug.Router, [my_config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()]}, + + | options: [port: 4040]} + + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.("{Bandit, scheme: :http, plug: TestPlug.Router, port: 4040}") + } + test "configures Bandit app", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Bandit, scheme: :http, plug: TestPlug.Router, port: 4040} + + | {Bandit, + + | scheme: :http, plug: {TestPlug.Router, phoenix_sync: Phoenix.Sync.plug_opts()}, port: 4040} + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Bandit, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, port: 4040}" + ) + } + test "configures Bandit app with existing config", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Bandit, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, port: 4040} + + | {Bandit, + + | scheme: :http, + + | plug: {TestPlug.Router, [my_config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()]}, + + | port: 4040} + + """ + ) + end + + @tag files: %{ + "config/config.exs" => """ + import Config + + import_config "\#{config_env()}.exs" + """, + "config/test.exs" => """ + import Config + """ + } + test "updates existing config files", ctx do + ctx.igniter + |> assert_has_patch( + "config/config.exs", + """ + + |config :phoenix_sync, mode: :embedded, env: config_env(), repo: TestPlug.Repo + """ + ) + |> assert_has_patch( + "config/test.exs", + """ + + |config :phoenix_sync, mode: :sandbox, env: config_env() + """ + ) + end + + test "creates config files", ctx do + ctx.igniter + |> assert_creates( + "config/config.exs", + """ + import Config + config :phoenix_sync, mode: :embedded, env: config_env(), repo: TestPlug.Repo + import_config "\#{config_env()}.exs" + """ + ) + |> assert_creates( + "config/test.exs", + """ + import Config + config :phoenix_sync, mode: :sandbox, env: config_env() + """ + ) + end + + test "adds the sandbox config to the repo", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/repo.ex", + """ + + | use Phoenix.Sync.Sandbox.Postgres + + | + """ + ) + |> assert_has_patch( + "lib/test_plug/repo.ex", + """ + - | adapter: Ecto.Adapters.Postgres + + | adapter: Phoenix.Sync.Sandbox.Postgres.adapter() + """ + ) + end + + @tag install_args: ["--sync-mode", "embedded", "--no-sync-sandbox"] + test "honours --no-sync-sandbox", ctx do + ctx.igniter + |> assert_unchanged("lib/test_plug/repo.ex") + end + + @tag files: %{"lib/test_plug/repo.ex" => ""} + test "handles a repo-less application", ctx do + ctx.igniter + |> assert_unchanged("lib/test_plug/repo.ex") + |> assert_creates( + "config/config.exs", + """ + import Config + + config :phoenix_sync, + mode: :embedded, + env: config_env(), + # add your real database connection details + connection_opts: [ + username: "your_username", + password: "your_password", + hostname: "localhost", + database: "your_database", + port: 5432, + # sslmode can be: :disable, :allow, :prefer or :require + sslmode: :prefer + ] + """ + ) + |> refute_creates("config/test.exs") + end + end + + describe "Plug: http" do + @describetag install_args: [ + "--sync-mode", + "http", + "--sync-url", + "https://electric-sql.cloud/" + ] + + setup(ctx) do + extra_files = Map.get(ctx, :files, %{}) + + files = Map.put(Map.merge(@plug_files, extra_files), "lib/test_plug/repo.ex", "") + + [igniter: run_install_task(test_project(files: files), ctx)] + end + + test "adds electric dependency", ctx do + ctx.igniter + |> assert_unchanged("mix.exs") + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Plug.Cowboy, scheme: :http, plug: TestPlug.Router, options: [port: 4040]}" + ) + } + test "configures Plug.Cowboy app", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Plug.Cowboy, scheme: :http, plug: TestPlug.Router, options: [port: 4040]} + + | {Plug.Cowboy, + + | scheme: :http, + + | plug: {TestPlug.Router, phoenix_sync: Phoenix.Sync.plug_opts()}, + + | options: [port: 4040]} + + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Plug.Cowboy, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, options: [port: 4040]}" + ) + } + test "configures Plug.Cowboy app which already has config", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Plug.Cowboy, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, options: [port: 4040]} + + | {Plug.Cowboy, + + | scheme: :http, + + | plug: {TestPlug.Router, [my_config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()]}, + + | options: [port: 4040]} + + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => """ + defmodule TestPlug.Application do + use Application + + alias TestPlug.Router + + def start(_type, _args) do + children = [ + {Plug.Cowboy, scheme: :http, plug: {Router, my_config: [here: true]}, options: [port: 4040]} + ] + + opts = [strategy: :one_for_one, name: TestPlug.Supervisor] + Supervisor.start_link(children, opts) + end + end + """ + } + test "supports aliased plug modules", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Plug.Cowboy, scheme: :http, plug: {Router, my_config: [here: true]}, options: [port: 4040]} + + | {Plug.Cowboy, + + | scheme: :http, + + | plug: {Router, [my_config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()]}, + + | options: [port: 4040]} + + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.("{Bandit, scheme: :http, plug: TestPlug.Router, port: 4040}") + } + test "configures Bandit app", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Bandit, scheme: :http, plug: TestPlug.Router, port: 4040} + + | {Bandit, + + | scheme: :http, plug: {TestPlug.Router, phoenix_sync: Phoenix.Sync.plug_opts()}, port: 4040} + """ + ) + end + + @tag files: %{ + "lib/test_plug/application.ex" => + plug_application.( + "{Bandit, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, port: 4040}" + ) + } + test "configures Bandit app with existing config", ctx do + ctx.igniter + |> assert_has_patch( + "lib/test_plug/application.ex", + """ + - | {Bandit, scheme: :http, plug: {TestPlug.Router, my_config: [here: true]}, port: 4040} + + | {Bandit, + + | scheme: :http, + + | plug: {TestPlug.Router, [my_config: [here: true], phoenix_sync: Phoenix.Sync.plug_opts()]}, + + | port: 4040} + + """ + ) + end + + @tag files: %{ + "config/config.exs" => """ + import Config + + import_config "\#{config_env()}.exs" + """, + "config/test.exs" => """ + import Config + """ + } + test "updates existing config files", ctx do + ctx.igniter + |> assert_has_patch( + "config/config.exs", + """ + + |config :phoenix_sync, + + | mode: :http, + + | env: config_env(), + + | url: "https://electric-sql.cloud/", + + | credentials: [secret: "MY_SECRET", source_id: "00000000-0000-0000-0000-000000000000"] + + | + """ + ) + |> assert_unchanged("config/test.exs") + end + + test "creates config files", ctx do + ctx.igniter + |> assert_creates( + "config/config.exs", + """ + import Config + + config :phoenix_sync, + mode: :http, + env: config_env(), + url: "https://electric-sql.cloud/", + credentials: [secret: "MY_SECRET", source_id: "00000000-0000-0000-0000-000000000000"] + """ + ) + |> refute_creates("config/test.exs") + end + end +end