diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ec7121..a043095 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,22 @@ on: workflow_call: jobs: + lint: + runs-on: ubuntu-latest + name: Linter + env: + MIX_ENV: test + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 26.0 + elixir-version: 1.15 + - run: mix deps.get + - run: mix compile --warnings-as-errors + - run: mix credo --strict --ignore design.alias + - run: mix format --check-formatted + test: services: postgres: @@ -40,5 +56,4 @@ jobs: elixir-version: ${{matrix.version.elixir}} - run: mix deps.get mix compile --warnings-as-errors - run: mix test - - run: mix credo diff --git a/lib/idempotency_plug.ex b/lib/idempotency_plug.ex index cd5cacf..0b1853e 100644 --- a/lib/idempotency_plug.ex +++ b/lib/idempotency_plug.ex @@ -4,10 +4,9 @@ defmodule IdempotencyPlug do There's no Idempotency-Key request headers. """ - defexception [ - message: "No idempotency key found. You need to set the `Idempotency-Key` header for all POST requests: 'Idempotency-Key: KEY'", - plug_status: :bad_request - ] + defexception message: + "No idempotency key found. You need to set the `Idempotency-Key` header for all POST requests: 'Idempotency-Key: KEY'", + plug_status: :bad_request end defmodule MultipleHeadersError do @@ -15,10 +14,8 @@ defmodule IdempotencyPlug do There are multiple Idempotency-Key request headers. """ - defexception [ - message: "Only one `Idempotency-Key` header can be sent", - plug_status: :bad_request - ] + defexception message: "Only one `Idempotency-Key` header can be sent", + plug_status: :bad_request end defmodule ConcurrentRequestError do @@ -26,10 +23,9 @@ defmodule IdempotencyPlug do There's another request currently being processed for this ID. """ - defexception [ - message: "A request with the same `Idempotency-Key` is currently being processed", - plug_status: :conflict - ] + defexception message: + "A request with the same `Idempotency-Key` is currently being processed", + plug_status: :conflict end defmodule RequestPayloadFingerprintMismatchError do @@ -51,18 +47,20 @@ defmodule IdempotencyPlug do defexception [ :reason, - message: "The original request was interrupted and can't be recovered as it's in an unknown state", + message: + "The original request was interrupted and can't be recovered as it's in an unknown state", plug_status: :internal_server_error ] end - defimpl Plug.Exception, for: [ - NoHeadersError, - MultipleHeadersError, - ConcurrentRequestError, - RequestPayloadFingerprintMismatchError, - HaltedResponseError - ] do + defimpl Plug.Exception, + for: [ + NoHeadersError, + MultipleHeadersError, + ConcurrentRequestError, + RequestPayloadFingerprintMismatchError, + HaltedResponseError + ] do def status(%{plug_status: status}), do: Plug.Conn.Status.code(status) def actions(_), do: [] end @@ -173,7 +171,7 @@ defmodule IdempotencyPlug do other -> raise ArgumentError, - "option :tracker must be one of PID or Atom, got: #{inspect(other)}" + "option :tracker must be one of PID or Atom, got: #{inspect(other)}" end opts @@ -208,7 +206,7 @@ defmodule IdempotencyPlug do other -> # credo:disable-for-next-line Credo.Check.Warning.RaiseInsideRescue raise ArgumentError, - "option :with should be one of :exception or MFA, got: #{inspect(other)}" + "option :with should be one of :exception or MFA, got: #{inspect(other)}" end end @@ -254,9 +252,14 @@ defmodule IdempotencyPlug do processed_key = case Keyword.get(opts, :idempotency_key, {__MODULE__, :idempotency_key}) do - {mod, fun} -> apply(mod, fun, [conn, key]) - {mod, fun, args} -> apply(mod, fun, [conn, key | args]) - other -> raise ArgumentError, "option :idempotency_key must be a MFA, got: #{inspect(other)}" + {mod, fun} -> + apply(mod, fun, [conn, key]) + + {mod, fun, args} -> + apply(mod, fun, [conn, key | args]) + + other -> + raise ArgumentError, "option :idempotency_key must be a MFA, got: #{inspect(other)}" end hash(:idempotency_key, processed_key, opts) @@ -275,9 +278,15 @@ defmodule IdempotencyPlug do defp hash_request_payload(conn, opts) do payload = case Keyword.get(opts, :request_payload, {__MODULE__, :request_payload}) do - {mod, fun} -> apply(mod, fun, [conn]) - {mod, fun, args} -> apply(mod, fun, [conn | args]) - other -> raise ArgumentError, "option :request_payload must be a MFA tuple, got: #{inspect(other)}" + {mod, fun} -> + apply(mod, fun, [conn]) + + {mod, fun, args} -> + apply(mod, fun, [conn | args]) + + other -> + raise ArgumentError, + "option :request_payload must be a MFA tuple, got: #{inspect(other)}" end hash(:request_payload, payload, opts) @@ -308,7 +317,7 @@ defmodule IdempotencyPlug do Conn.register_before_send(conn, fn conn -> case RequestTracker.put_response(tracker, key, conn_to_response(conn)) do {:ok, expires} -> put_expires_header(conn, expires) - {:error, error} -> raise "failed to put response in cache store, got: #{inspect error}" + {:error, error} -> raise "failed to put response in cache store, got: #{inspect(error)}" end end) end @@ -338,8 +347,11 @@ defmodule IdempotencyPlug do mod |> apply(fun, [conn, error | args]) |> case do - %Conn{halted: true} = conn -> conn - other -> raise ArgumentError, "option :with MUST return a halted conn, got: #{inspect(other)}" + %Conn{halted: true} = conn -> + conn + + other -> + raise ArgumentError, "option :with MUST return a halted conn, got: #{inspect(other)}" end end end diff --git a/lib/idempotency_plug/request_tracker.ex b/lib/idempotency_plug/request_tracker.ex index 4388381..594fbda 100644 --- a/lib/idempotency_plug/request_tracker.ex +++ b/lib/idempotency_plug/request_tracker.ex @@ -87,12 +87,12 @@ defmodule IdempotencyPlug.RequestTracker do fingerprint differs from what was stored, an error is returned. """ @spec track(atom() | pid(), binary(), binary()) :: - {:error, term()} | - {:init, binary(), DateTime.t()} | - {:mismatch, {:fingerprint, binary()}, DateTime.t()} | - {:processing, {atom(), pid()}, DateTime.t()} | - {:cache, {:ok, any()}, DateTime.t()} | - {:cache, {:halted, term()}, DateTime.t()} + {:error, term()} + | {:init, binary(), DateTime.t()} + | {:mismatch, {:fingerprint, binary()}, DateTime.t()} + | {:processing, {atom(), pid()}, DateTime.t()} + | {:cache, {:ok, any()}, DateTime.t()} + | {:cache, {:halted, term()}, DateTime.t()} def track(name_or_pid, request_id, fingerprint) do GenServer.call(name_or_pid, {:track, request_id, fingerprint}) end @@ -162,7 +162,7 @@ defmodule IdempotencyPlug.RequestTracker do def handle_call({:put_response, request_id, response}, _from, state) do {store, store_opts} = fetch_store(state.options) - {_finished, state} = pop_monitored(state, &elem(&1, 0) == request_id) + {_finished, state} = pop_monitored(state, &(elem(&1, 0) == request_id)) data = {:ok, response} expires_at = expires_at(state.options) @@ -189,7 +189,7 @@ defmodule IdempotencyPlug.RequestTracker do @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, state) do {store, store_opts} = fetch_store(state.options) - {finished, state} = pop_monitored(state, &elem(&1, 1) == pid) + {finished, state} = pop_monitored(state, &(elem(&1, 1) == pid)) data = {:halted, reason} expires_at = expires_at(state.options) diff --git a/lib/idempotency_plug/store.ex b/lib/idempotency_plug/store.ex index 92ae1bb..cf623b7 100644 --- a/lib/idempotency_plug/store.ex +++ b/lib/idempotency_plug/store.ex @@ -31,7 +31,8 @@ defmodule IdempotencyPlug.Store do @callback setup(options()) :: :ok | {:error, term()} @callback lookup(request_id(), options()) :: {data(), fingerprint(), expires_at()} | :not_found - @callback insert(request_id(), data(), fingerprint(), expires_at(), options()) :: :ok | {:error, term()} + @callback insert(request_id(), data(), fingerprint(), expires_at(), options()) :: + :ok | {:error, term()} @callback update(request_id(), data(), expires_at(), options()) :: :ok | {:error, term()} @callback prune(options()) :: :ok end diff --git a/lib/idempotency_plug/store/ecto_store.ex b/lib/idempotency_plug/store/ecto_store.ex index 67598f5..4ad424a 100644 --- a/lib/idempotency_plug/store/ecto_store.ex +++ b/lib/idempotency_plug/store/ecto_store.ex @@ -1,149 +1,149 @@ if Code.ensure_loaded?(Ecto) do -defmodule IdempotencyPlug.EctoStore do - @moduledoc """ - Module that defines an Ecto store. + defmodule IdempotencyPlug.EctoStore do + @moduledoc """ + Module that defines an Ecto store. - A migration file should be generated with - `mix idempotency_plug.ecto.gen.migration`. + A migration file should be generated with + `mix idempotency_plug.ecto.gen.migration`. - ## Examples + ## Examples - defmodule MyApp.Application do - # .. + defmodule MyApp.Application do + # .. - def start(_type, _args) do - children = [ - {IdempotencyPlug.RequestTracker, [ - store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo}]} - # ... - ] + def start(_type, _args) do + children = [ + {IdempotencyPlug.RequestTracker, [ + store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo}]} + # ... + ] - Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) + Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) + end end - end - """ + """ - defmodule ErlangTerm do - @moduledoc false - use Ecto.Type + defmodule ErlangTerm do + @moduledoc false + use Ecto.Type - @impl true - def type, do: :binary + @impl true + def type, do: :binary - @impl true - def cast(term), do: {:ok, term} + @impl true + def cast(term), do: {:ok, term} - @impl true - def load(bin) when is_binary(bin), do: {:ok, :erlang.binary_to_term(bin)} + @impl true + def load(bin) when is_binary(bin), do: {:ok, :erlang.binary_to_term(bin)} - @impl true - def dump(term), do: {:ok, :erlang.term_to_binary(term)} - end + @impl true + def dump(term), do: {:ok, :erlang.term_to_binary(term)} + end - defmodule IdempotentRequest do - @moduledoc false - use Ecto.Schema + defmodule IdempotentRequest do + @moduledoc false + use Ecto.Schema - import Ecto.Changeset + import Ecto.Changeset - @primary_key {:id, :string, autogenerate: false} - @timestamps_opts [type: :utc_datetime_usec] + @primary_key {:id, :string, autogenerate: false} + @timestamps_opts [type: :utc_datetime_usec] - schema "idempotency_plug_requests" do - field :fingerprint, :string - field :data, ErlangTerm - field :expires_at, :utc_datetime_usec + schema "idempotency_plug_requests" do + field(:fingerprint, :string) + field(:data, ErlangTerm) + field(:expires_at, :utc_datetime_usec) - timestamps() - end + timestamps() + end - def changeset(struct) do - struct - |> change() - |> unique_constraint(:id, name: :idempotency_plug_requests_pkey) + def changeset(struct) do + struct + |> change() + |> unique_constraint(:id, name: :idempotency_plug_requests_pkey) + end end - end - @behaviour IdempotencyPlug.Store + @behaviour IdempotencyPlug.Store - import Ecto.Query + import Ecto.Query - @impl true - def setup(opts) do - case repo(opts) do - {:ok, repo} -> - # This will raise an error if the migration haven't been generate - # for the repo - repo.exists?(IdempotentRequest) + @impl true + def setup(opts) do + case repo(opts) do + {:ok, repo} -> + # This will raise an error if the migration haven't been generate + # for the repo + repo.exists?(IdempotentRequest) - :ok + :ok - {:error, error} -> - {:error, error} + {:error, error} -> + {:error, error} + end end - end - @impl true - def lookup(request_id, opts) do - case repo!(opts).get(IdempotentRequest, request_id) do - nil -> :not_found - request -> {request.data, request.fingerprint, request.expires_at} + @impl true + def lookup(request_id, opts) do + case repo!(opts).get(IdempotentRequest, request_id) do + nil -> :not_found + request -> {request.data, request.fingerprint, request.expires_at} + end end - end - @impl true - def insert(request_id, data, fingerprint, expires_at, opts) do - changeset = - IdempotentRequest.changeset(%IdempotentRequest{ - id: request_id, - data: data, - fingerprint: fingerprint, - expires_at: expires_at - }) - - case repo!(opts).insert(changeset) do - {:ok, _} -> :ok - {:error, error} -> {:error, error} + @impl true + def insert(request_id, data, fingerprint, expires_at, opts) do + changeset = + IdempotentRequest.changeset(%IdempotentRequest{ + id: request_id, + data: data, + fingerprint: fingerprint, + expires_at: expires_at + }) + + case repo!(opts).insert(changeset) do + {:ok, _} -> :ok + {:error, error} -> {:error, error} + end end - end - @impl true - def update(request_id, data, expires_at, opts) do - repo = repo!(opts) - updates = [set: [data: data, expires_at: expires_at, updated_at: DateTime.utc_now()]] - - IdempotentRequest - |> where(id: ^request_id) - |> repo.update_all(updates) - |> case do - {1, _} -> :ok - {0, _} -> {:error, "key #{request_id} not found in store"} + @impl true + def update(request_id, data, expires_at, opts) do + repo = repo!(opts) + updates = [set: [data: data, expires_at: expires_at, updated_at: DateTime.utc_now()]] + + IdempotentRequest + |> where(id: ^request_id) + |> repo.update_all(updates) + |> case do + {1, _} -> :ok + {0, _} -> {:error, "key #{request_id} not found in store"} + end end - end - @impl true - def prune(opts) do - repo = repo!(opts) + @impl true + def prune(opts) do + repo = repo!(opts) - IdempotentRequest - |> where([r], r.expires_at < ^DateTime.utc_now()) - |> repo.delete_all() + IdempotentRequest + |> where([r], r.expires_at < ^DateTime.utc_now()) + |> repo.delete_all() - :ok - end + :ok + end - defp repo(opts) do - case Keyword.fetch(opts, :repo) do - {:ok, repo} -> {:ok, repo} - :error -> {:error, ":repo must be specified in options for #{inspect __MODULE__}"} + defp repo(opts) do + case Keyword.fetch(opts, :repo) do + {:ok, repo} -> {:ok, repo} + :error -> {:error, ":repo must be specified in options for #{inspect(__MODULE__)}"} + end end - end - defp repo!(opts) do - case repo(opts) do - {:ok, repo} -> repo - {:error, error} -> raise error + defp repo!(opts) do + case repo(opts) do + {:ok, repo} -> repo + {:error, error} -> raise error + end end end end -end diff --git a/lib/idempotency_plug/store/ets_store.ex b/lib/idempotency_plug/store/ets_store.ex index 509a6ea..9ff2c7f 100644 --- a/lib/idempotency_plug/store/ets_store.ex +++ b/lib/idempotency_plug/store/ets_store.ex @@ -82,7 +82,7 @@ defmodule IdempotencyPlug.ETSStore do defp table(opts) do case Keyword.fetch(opts, :table) do {:ok, table} -> {:ok, table} - :error -> {:error, ":table must be specified in options for #{inspect __MODULE__}"} + :error -> {:error, ":table must be specified in options for #{inspect(__MODULE__)}"} end end diff --git a/lib/mix/tasks/idempotency_plug.ecto.gen.migration.ex b/lib/mix/tasks/idempotency_plug.ecto.gen.migration.ex index 32cf703..4b83245 100644 --- a/lib/mix/tasks/idempotency_plug.ecto.gen.migration.ex +++ b/lib/mix/tasks/idempotency_plug.ecto.gen.migration.ex @@ -1,42 +1,42 @@ if Code.ensure_loaded?(Mix.Tasks.Ecto.Gen.Migration) do -defmodule Mix.Tasks.IdempotencyPlug.Ecto.Gen.Migration do - @moduledoc """ - Generates a IdempotencyPlug store migration. + defmodule Mix.Tasks.IdempotencyPlug.Ecto.Gen.Migration do + @moduledoc """ + Generates a IdempotencyPlug store migration. - See `Mix.Tasks.Ecto.Gen.Migration` for options, takes all options - except `--change`. - """ - @shortdoc "Generates a new IdempotencyPlug store migration for the repo" - use Mix.Task + See `Mix.Tasks.Ecto.Gen.Migration` for options, takes all options + except `--change`. + """ + @shortdoc "Generates a new IdempotencyPlug store migration for the repo" + use Mix.Task - alias Mix.Tasks.Ecto.Gen.Migration + alias Mix.Tasks.Ecto.Gen.Migration - @impl true - def run(args) do - args = - case OptionParser.parse!(args, switches: []) do - {_, [_name | _rest]} -> Mix.raise "Do not define a table name" - {_, []} -> args ++ ["idempotency_plug_requests"] - end + @impl true + def run(args) do + args = + case OptionParser.parse!(args, switches: []) do + {_, [_name | _rest]} -> Mix.raise("Do not define a table name") + {_, []} -> args ++ ["idempotency_plug_requests"] + end - if "--change" in args do - Mix.raise "--change flag is not allowed" - else - change = - """ - # Used by IdempotencyPlug.EctoStore - create table(:idempotency_plug_requests, primary_key: false) do - add :id, :string, primary_key: true - add :fingerprint, :string, null: false - add :data, :binary, null: false - add :expires_at, :utc_datetime_usec, null: false + if "--change" in args do + Mix.raise("--change flag is not allowed") + else + change = + """ + # Used by IdempotencyPlug.EctoStore + create table(:idempotency_plug_requests, primary_key: false) do + add :id, :string, primary_key: true + add :fingerprint, :string, null: false + add :data, :binary, null: false + add :expires_at, :utc_datetime_usec, null: false - timestamps() - end - """ + timestamps() + end + """ - Migration.run(args ++ ["--change", change]) + Migration.run(args ++ ["--change", change]) + end end end end -end diff --git a/mix.exs b/mix.exs index 00ff7e7..38ef1d1 100644 --- a/mix.exs +++ b/mix.exs @@ -37,6 +37,7 @@ defmodule IdempotencyPlug.MixProject do {:ecto, "~> 3.9", optional: true}, {:ecto_sql, "~> 3.9", optional: true}, + # Development and test {:postgrex, ">= 0.0.0", only: [:test]}, {:credo, ">= 0.0.0", only: [:dev, :test]}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} diff --git a/test/idempotency_plug/request_tracker_test.exs b/test/idempotency_plug/request_tracker_test.exs index 56231b7..3945bd1 100644 --- a/test/idempotency_plug/request_tracker_test.exs +++ b/test/idempotency_plug/request_tracker_test.exs @@ -40,14 +40,16 @@ defmodule IdempotencyPlug.RequestTrackerTest do receive do {:expires, expires} -> - assert {:processing, _node_caller, ^expires} = RequestTracker.track(pid, "concurrent-request", "fingerprint") + assert {:processing, _node_caller, ^expires} = + RequestTracker.track(pid, "concurrent-request", "fingerprint") end send(task.pid, :continue) receive do {:expires, expires} -> - assert {:cache, {:ok, "OK"}, ^expires} = RequestTracker.track(pid, "concurrent-request", "fingerprint") + assert {:cache, {:ok, "OK"}, ^expires} = + RequestTracker.track(pid, "concurrent-request", "fingerprint") end end @@ -55,14 +57,16 @@ defmodule IdempotencyPlug.RequestTrackerTest do {:init, key, _expires} = RequestTracker.track(pid, "cached-fingerprint", "fingerprint") {:ok, expires} = RequestTracker.put_response(pid, key, "OK") - assert {:mismatch, {:fingerprint, "fingerprint"}, ^expires} = RequestTracker.track(pid, "cached-fingerprint", "other-fingerprint") + assert {:mismatch, {:fingerprint, "fingerprint"}, ^expires} = + RequestTracker.track(pid, "cached-fingerprint", "other-fingerprint") end test "with cached response", %{pid: pid} do {:init, key, _expires} = RequestTracker.track(pid, "cached-response", "fingerprint") {:ok, expires} = RequestTracker.put_response(pid, key, "OK") - assert {:cache, {:ok, "OK"}, ^expires} = RequestTracker.track(pid, "cached-response", "fingerprint") + assert {:cache, {:ok, "OK"}, ^expires} = + RequestTracker.track(pid, "cached-response", "fingerprint") end @tag capture_log: true @@ -78,18 +82,21 @@ defmodule IdempotencyPlug.RequestTrackerTest do {{%RuntimeError{message: "oops"}, _}, _} = catch_exit(Task.await(task)) assert {:cache, {:halted, {%RuntimeError{message: "oops"}, _}}, _expires} = - RequestTracker.track(pid, "halted-request", "fingerprint") + RequestTracker.track(pid, "halted-request", "fingerprint") end test "when no tracked request", %{pid: pid} do - assert {:error, "key no-request not found in store"} = RequestTracker.put_response(pid, "no-request", "OK") + assert {:error, "key no-request not found in store"} = + RequestTracker.put_response(pid, "no-request", "OK") end @tag options: [prune: 5, cache_ttl: 10] test "prunes", %{pid: pid} do {:init, _id, _expires} = RequestTracker.track(pid, "prune", "fingerprint") - assert {:processing, _node_caller, _expires} = RequestTracker.track(pid, "prune", "fingerprint") + assert {:processing, _node_caller, _expires} = + RequestTracker.track(pid, "prune", "fingerprint") + :timer.sleep(20) assert {:init, _id, _expires} = RequestTracker.track(pid, "prune", "fingerprint") end diff --git a/test/idempotency_plug/store/ecto_store_test.exs b/test/idempotency_plug/store/ecto_store_test.exs index bbcfc89..1e3c06c 100644 --- a/test/idempotency_plug/store/ecto_store_test.exs +++ b/test/idempotency_plug/store/ecto_store_test.exs @@ -26,7 +26,7 @@ defmodule IdempotencyPlug.EctoStoreTest do assert EctoStore.setup(@options) == :ok assert EctoStore.setup([]) == - {:error, ":repo must be specified in options for IdempotencyPlug.EctoStore"} + {:error, ":repo must be specified in options for IdempotencyPlug.EctoStore"} end test "inserts, looks up, and updates" do @@ -36,16 +36,21 @@ defmodule IdempotencyPlug.EctoStoreTest do expires_at = DateTime.utc_now() assert EctoStore.insert(@request_id, @data, @fingerprint, expires_at, @options) == :ok - assert {:error, _changeset} = EctoStore.insert(@request_id, @data, @fingerprint, expires_at, @options) + + assert {:error, _changeset} = + EctoStore.insert(@request_id, @data, @fingerprint, expires_at, @options) assert EctoStore.lookup(@request_id, @options) == {@data, @fingerprint, expires_at} updated_expires_at = DateTime.utc_now() - assert EctoStore.update(@other_request_id, @updated_data, updated_expires_at, @options) == {:error, "key #{@other_request_id} not found in store"} + + assert EctoStore.update(@other_request_id, @updated_data, updated_expires_at, @options) == + {:error, "key #{@other_request_id} not found in store"} assert EctoStore.update(@request_id, @updated_data, updated_expires_at, @options) == :ok - assert EctoStore.lookup(@request_id, @options) == {@updated_data, @fingerprint, updated_expires_at} + assert EctoStore.lookup(@request_id, @options) == + {@updated_data, @fingerprint, updated_expires_at} end test "prunes" do @@ -75,20 +80,25 @@ defmodule IdempotencyPlug.EctoStoreTest do pool: Ecto.Adapters.SQL.Sandbox, priv: "priv/repo", log: false, - url: System.get_env("POSTGRES_URL")) + url: System.get_env("POSTGRES_URL") + ) capture_io(fn -> Mix.Task.run("idempotency_plug.ecto.gen.migration", ["-r", inspect(TestRepo)]) end) - TestRepo.__adapter__.storage_down(TestRepo.config()) - :ok = TestRepo.__adapter__.storage_up(TestRepo.config()) + TestRepo.__adapter__().storage_down(TestRepo.config()) + :ok = TestRepo.__adapter__().storage_up(TestRepo.config()) start_supervised!(TestRepo) migrations_path = Path.join("priv", "repo") - {:ok, _, _} = Ecto.Migrator.with_repo(TestRepo, &Ecto.Migrator.run(&1, migrations_path, :up, all: true, log: false)) + {:ok, _, _} = + Ecto.Migrator.with_repo( + TestRepo, + &Ecto.Migrator.run(&1, migrations_path, :up, all: true, log: false) + ) # start_supervised!(TestRepo) Ecto.Adapters.SQL.Sandbox.mode(TestRepo, :manual) diff --git a/test/idempotency_plug/store/ets_store_test.exs b/test/idempotency_plug/store/ets_store_test.exs index 6dccb78..3230be3 100644 --- a/test/idempotency_plug/store/ets_store_test.exs +++ b/test/idempotency_plug/store/ets_store_test.exs @@ -16,7 +16,7 @@ defmodule IdempotencyPlug.ETSStoreTest do assert ETSStore.setup(@options) == :ok assert ETSStore.setup([]) == - {:error, ":table must be specified in options for IdempotencyPlug.ETSStore"} + {:error, ":table must be specified in options for IdempotencyPlug.ETSStore"} end test "inserts, looks up, and updates" do @@ -26,15 +26,21 @@ defmodule IdempotencyPlug.ETSStoreTest do expires_at = DateTime.utc_now() assert ETSStore.insert(@request_id, @data, @fingerprint, expires_at, @options) == :ok - assert ETSStore.insert(@request_id, @data, @fingerprint, expires_at, @options) == {:error, "key #{@request_id} already exists in store"} + + assert ETSStore.insert(@request_id, @data, @fingerprint, expires_at, @options) == + {:error, "key #{@request_id} already exists in store"} assert ETSStore.lookup(@request_id, @options) == {@data, @fingerprint, expires_at} updated_expires_at = DateTime.utc_now() - assert ETSStore.update(@other_request_id, @updated_data, updated_expires_at, @options) == {:error, "key #{@other_request_id} not found in store"} + + assert ETSStore.update(@other_request_id, @updated_data, updated_expires_at, @options) == + {:error, "key #{@other_request_id} not found in store"} + assert ETSStore.update(@request_id, @updated_data, updated_expires_at, @options) == :ok - assert ETSStore.lookup(@request_id, @options) == {@updated_data, @fingerprint, updated_expires_at} + assert ETSStore.lookup(@request_id, @options) == + {@updated_data, @fingerprint, updated_expires_at} end test "prunes" do diff --git a/test/idempotency_plug_test.exs b/test/idempotency_plug_test.exs index b07b597..5e0c230 100644 --- a/test/idempotency_plug_test.exs +++ b/test/idempotency_plug_test.exs @@ -14,9 +14,11 @@ defmodule IdempotencyPlugTest do end test "with invalid tracker" do - assert_raise ArgumentError, "option :tracker must be one of PID or Atom, got: \"invalid\"", fn -> - IdempotencyPlug.init(tracker: "invalid") - end + assert_raise ArgumentError, + "option :tracker must be one of PID or Atom, got: \"invalid\"", + fn -> + IdempotencyPlug.init(tracker: "invalid") + end end test "with no idempotency header set", %{conn: conn, tracker: tracker} do @@ -69,13 +71,17 @@ defmodule IdempotencyPlugTest do task = Task.async(fn -> - run_plug(conn, tracker, callback: fn conn -> - send(pid, :continue) - receive do - :continue -> :ok + run_plug(conn, tracker, + callback: fn conn -> + send(pid, :continue) + + receive do + :continue -> :ok + end + + conn end - conn - end) + ) end) receive do @@ -88,7 +94,9 @@ defmodule IdempotencyPlugTest do end assert Plug.Exception.status(error) == 409 - assert error.message =~ "A request with the same `Idempotency-Key` is currently being processed" + + assert error.message =~ + "A request with the same `Idempotency-Key` is currently being processed" send(task.pid, :continue) Task.await(task) @@ -106,16 +114,20 @@ defmodule IdempotencyPlugTest do end assert Plug.Exception.status(error) == 500 - assert error.message =~ "The original request was interrupted and can't be recovered as it's in an unknown state" + + assert error.message =~ + "The original request was interrupted and can't be recovered as it's in an unknown state" end test "with cached response", %{conn: conn, tracker: tracker} do other_conn = - run_plug(conn, tracker, callback: fn conn -> - conn - |> put_resp_header("x-header-key", "header-value") - |> send_resp(201, "OTHER") - end) + run_plug(conn, tracker, + callback: fn conn -> + conn + |> put_resp_header("x-header-key", "header-value") + |> send_resp(201, "OTHER") + end + ) conn = run_plug(conn, tracker) @@ -138,7 +150,9 @@ defmodule IdempotencyPlugTest do end assert Plug.Exception.status(error) == 422 - assert error.message =~ "This `Idempotency-Key` can't be reused with a different payload or URI" + + assert error.message =~ + "This `Idempotency-Key` can't be reused with a different payload or URI" end test "with cached response with different request URI", %{conn: conn, tracker: tracker} do @@ -229,9 +243,11 @@ defmodule IdempotencyPlugTest do end test "with invalid `:request_payload`", %{conn: conn, tracker: tracker} do - assert_raise ArgumentError, "option :request_payload must be a MFA tuple, got: :invalid", fn -> - run_plug(conn, tracker, request_payload: :invalid) - end + assert_raise ArgumentError, + "option :request_payload must be a MFA tuple, got: :invalid", + fn -> + run_plug(conn, tracker, request_payload: :invalid) + end end def scope_request_payload(conn, :arg1), do: Map.take(conn.params, ["a"]) @@ -256,22 +272,26 @@ defmodule IdempotencyPlugTest do error = assert_raise IdempotencyPlug.RequestPayloadFingerprintMismatchError, fn -> conn - |> Map.put(:params, %{"a" => 2}) + |> Map.put(:params, %{"a" => 2}) |> run_plug(tracker, opts) end assert Plug.Exception.status(error) == 422 - assert error.message =~ "This `Idempotency-Key` can't be reused with a different payload or URI" + + assert error.message =~ + "This `Idempotency-Key` can't be reused with a different payload or URI" end test "with invalid `:with`", %{conn: conn, tracker: tracker} do - assert_raise ArgumentError, "option :with should be one of :exception or MFA, got: :invalid", fn -> - conn - |> other_request_payload() - |> run_plug(tracker, with: :invalid) - - run_plug(conn, tracker, with: :invalid) - end + assert_raise ArgumentError, + "option :with should be one of :exception or MFA, got: :invalid", + fn -> + conn + |> other_request_payload() + |> run_plug(tracker, with: :invalid) + + run_plug(conn, tracker, with: :invalid) + end end def handle_error(conn, error, :arg1) do @@ -294,11 +314,15 @@ defmodule IdempotencyPlugTest do assert resp_conn.halted assert resp_conn.status == 422 - assert resp_conn.resp_body == "This `Idempotency-Key` can't be reused with a different payload or URI" - assert_raise ArgumentError, ~r/option :with MUST return a halted conn, got: %Plug.Conn{/, fn -> - run_plug(conn, tracker, with: {__MODULE__, :handle_error_unhalted}) - end + assert resp_conn.resp_body == + "This `Idempotency-Key` can't be reused with a different payload or URI" + + assert_raise ArgumentError, + ~r/option :with MUST return a halted conn, got: %Plug.Conn{/, + fn -> + run_plug(conn, tracker, with: {__MODULE__, :handle_error_unhalted}) + end end defp setup_tracker(_) do @@ -319,7 +343,7 @@ defmodule IdempotencyPlugTest do defp run_plug(conn, tracker, opts \\ []) do {callback, opts} = Keyword.pop(opts, :callback) - callback = callback || &send_resp(&1, 200, "OK") + callback = callback || (&send_resp(&1, 200, "OK")) conn |> IdempotencyPlug.call(IdempotencyPlug.init([tracker: tracker] ++ opts))