diff --git a/.formatter.exs b/.formatter.exs index d97672b..7f59b1f 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,12 +1,4 @@ -# Used by "mix format" and to export configuration. -export_locals_without_parens = [ - plug: 1, - plug: 2, - get: 2 -] - +# Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{integration,lib,test}/**/*.{ex,exs}"], - locals_without_parens: export_locals_without_parens, - export: [locals_without_parens: export_locals_without_parens] + inputs: ["{mix,.formatter}.exs", "{lib,test}/**/*.{ex,exs}"] ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9200b4b..cfe667a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,17 +19,44 @@ jobs: otp-version: 27.0 elixir-version: 1.18 - uses: actions/cache@v4 + env: + cache-name: assent with: path: | - _build/test + _build deps - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - run: mix deps.get - run: mix compile --warnings-as-errors - run: mix credo --strict --ignore design.tagtodo - run: mix format --check-formatted - run: mix dialyzer --format github + integration: + runs-on: ubuntu-latest + name: Integration server + defaults: + run: + working-directory: integration + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 27.0 + elixir-version: 1.18 + - uses: actions/cache@v4 + env: + cache-name: integration-server + with: + path: | + integration/_build + integration/deps + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/integration/mix.lock') }} + - run: mix deps.get + - run: mix compile --warnings-as-errors + - run: mix format --check-formatted + - run: mix dialyzer --format github + test: strategy: matrix: diff --git a/integration/.formatter.exs b/integration/.formatter.exs new file mode 100644 index 0000000..d97672b --- /dev/null +++ b/integration/.formatter.exs @@ -0,0 +1,12 @@ +# Used by "mix format" and to export configuration. +export_locals_without_parens = [ + plug: 1, + plug: 2, + get: 2 +] + +[ + inputs: ["{mix,.formatter}.exs", "{integration,lib,test}/**/*.{ex,exs}"], + locals_without_parens: export_locals_without_parens, + export: [locals_without_parens: export_locals_without_parens] +] diff --git a/integration/.gitignore b/integration/.gitignore new file mode 100644 index 0000000..671aba8 --- /dev/null +++ b/integration/.gitignore @@ -0,0 +1,23 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +project-*.tar + +# Temporary files, for example, from tests. +/tmp/ \ No newline at end of file diff --git a/integration/README.md b/integration/README.md index ec20fd3..8463bef 100644 --- a/integration/README.md +++ b/integration/README.md @@ -3,5 +3,5 @@ Simple plug-based server to test assent integration with all providers. ```bash -CLIENT_ID=CLIENT_ID CLIENT_SECRET=CLIENT_SECRET MIX_ENV=test mix run --no-halt integration/server.exs +mix run --no-halt ``` diff --git a/integration/lib/application.ex b/integration/lib/application.ex new file mode 100644 index 0000000..2bae81f --- /dev/null +++ b/integration/lib/application.ex @@ -0,0 +1,12 @@ +defmodule IntegrationServer.Application do + use Application + + def start(_type, _args) do + children = [ + {Bandit, plug: IntegrationServer.Router} + ] + + opts = [strategy: :one_for_one, name: IntegrationServer.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/integration/lib/router.ex b/integration/lib/router.ex new file mode 100644 index 0000000..4fb88e6 --- /dev/null +++ b/integration/lib/router.ex @@ -0,0 +1,122 @@ +defmodule IntegrationServer.Router do + use Plug.Router + use Plug.Debugger + use Plug.ErrorHandler + + require Logger + + import Plug.Conn + + @session_options [ + store: :cookie, + key: "_assent_integration_server", + signing_salt: "Q1OaP6Pz", + same_site: "Lax" + ] + + plug :secret_key_base + plug Plug.Session, @session_options + plug :fetch_session + plug :match + plug :dispatch + + {:ok, modules} = :application.get_key(:assent, :modules) + + @path_modules modules + |> Enum.map(&{Module.split(&1), &1}) + |> Enum.map(fn + {["Assent", "Strategy", uri], module} -> + path = Macro.underscore(uri) + fun_name = String.to_atom(path) + {path, module, fun_name} + + _any -> + nil + end) + |> Enum.reject(&is_nil/1) + + defp secret_key_base(conn, _opts) do + %{conn | secret_key_base: "LG8WiSkAlUlwVJpISmRYsi7aJV/Qlv65FXyxwWXxp1QUzQY3hzEfg73YKfKZPpe0"} + end + + for {path, module, fun_name} <- @path_modules do + @file "#{__ENV__.file}##{path}_auth" + defp unquote(fun_name)(conn, :auth) do + unquote(path) + |> config!() + |> unquote(module).authorize_url() + |> case do + {:ok, %{session_params: session_params, url: url}} -> + Logger.info("Redirecting to #{inspect(url)} with session params #{inspect(session_params)}") + + html = Plug.HTML.html_escape(url) + body = "You are being redirected." + + conn + |> put_session(:session_params, session_params) + |> put_resp_header("location", url) + |> put_resp_header("content-type", "text/html") + |> send_resp(302, body) + + {:error, error} -> + body = "An error occurred: #{inspect error}." + + conn + |> put_resp_header("content-type", "text/html") + |> send_resp(500, body) + end + end + + @file "#{__ENV__.file}##{path}_callback" + defp unquote(fun_name)(conn, :callback) do + conn = fetch_query_params(conn) + + unquote(path) + |> config!() + |> Assent.Config.put(:session_params, get_session(conn, :session_params)) + |> unquote(module).callback(conn.params) + |> case do + {:ok, %{user: user, token: token}} -> + body = "#{inspect(%{user: user, token: token})}" + + send_resp(conn, 200, body) + + {:error, error} -> + body = "An error occurred: #{inspect error}." + + conn + |> put_resp_header("content-type", "text/html") + |> send_resp(500, body) + end + end + end + + get "/" do + list = + @path_modules + |> Enum.map(&"
  • #{elem(&1, 0)}
  • ") + |> Enum.join() + + body = "" + + send_resp(conn, 200, body) + end + + for {path, _module, fun_name} <- @path_modules do + get "/#{path}", do: unquote(fun_name)(conn, :auth) + get "/#{path}/callback", do: unquote(fun_name)(conn, :callback) + end + + def config!(path) do + [ + client_id: System.fetch_env!("CLIENT_ID"), + client_secret: System.fetch_env!("CLIENT_SECRET"), + redirect_uri: "http://localhost:4000/#{path}/callback" + ] + end + + @impl true + def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do + send_resp(conn, conn.status, "Something went wrong") + end +end diff --git a/integration/mix.exs b/integration/mix.exs new file mode 100644 index 0000000..c675461 --- /dev/null +++ b/integration/mix.exs @@ -0,0 +1,32 @@ +defmodule IntegrationServer.MixProject do + use Mix.Project + + def project do + [ + app: :integration_server, + version: "0.0.1", + elixir: "~> 1.13", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + mod: {IntegrationServer.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + defp elixirc_paths(_), do: ["lib"] + + defp deps do + [ + {:assent, path: "../"}, + {:plug, ">= 0.0.0"}, + {:bandit, ">= 0.0.0"}, + {:dialyxir, "~> 1.4", runtime: false} + ] + end +end diff --git a/integration/mix.lock b/integration/mix.lock new file mode 100644 index 0000000..b351320 --- /dev/null +++ b/integration/mix.lock @@ -0,0 +1,12 @@ +%{ + "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, +} diff --git a/integration/server.exs b/integration/server.exs deleted file mode 100644 index 4172c9d..0000000 --- a/integration/server.exs +++ /dev/null @@ -1,96 +0,0 @@ -defmodule IntegrationServer.Router do - use Plug.Router - use Plug.Debugger - use Plug.ErrorHandler - - require Logger - - import Plug.Conn - - @session_options [ - store: :cookie, - key: "_assent_integration_server", - signing_salt: "Q1OaP6Pz", - same_site: "Lax" - ] - - plug :secret_key_base - plug Plug.Session, @session_options - plug :fetch_session - plug :match - plug :dispatch - - {:ok, modules} = :application.get_key(:assent, :modules) - - @path_modules modules - |> Enum.map(&{Module.split(&1), &1}) - |> Enum.map(fn - {["Assent", "Strategy", uri], module} -> {uri, module} - _any -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.into(%{}) - - defp secret_key_base(conn, _opts) do - %{conn | secret_key_base: "LG8WiSkAlUlwVJpISmRYsi7aJV/Qlv65FXyxwWXxp1QUzQY3hzEfg73YKfKZPpe0"} - end - - get "/" do - list = - @path_modules - |> Enum.map(&"
  • #{elem(&1, 0)}
  • ") - |> Enum.join() - - body = "" - - send_resp(conn, 200, body) - end - - get "/:provider" do - module = Map.fetch!(@path_modules, provider) - - {:ok, %{url: url, session_params: session_params}} = module.authorize_url(config!(provider)) - - Logger.info("Redirecting to #{inspect(url)} with session params #{inspect(session_params)}") - - html = Plug.HTML.html_escape(url) - body = "You are being redirected." - - conn - |> put_session(:session_params, session_params) - |> put_resp_header("location", url) - |> put_resp_header("content-type", "text/html") - |> send_resp(302, body) - end - - get "/:provider/callback" do - module = Map.fetch!(@path_modules, provider) - - conn = fetch_query_params(conn) - - {:ok, %{user: user, token: token}} = - provider - |> config!() - |> Assent.Config.put(:session_params, get_session(conn, :session_params)) - |> module.callback(conn.params) - - body = "#{inspect(%{user: user, token: token})}" - - send_resp(conn, 200, body) - end - - def config!(provider) do - [ - client_id: System.fetch_env!("CLIENT_ID"), - client_secret: System.fetch_env!("CLIENT_SECRET"), - redirect_uri: "http://localhost:4000/#{provider}/callback" - ] - end - - defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do - send_resp(conn, conn.status, "Something went wrong") - end -end - -{:ok, pid} = Bandit.start_link(plug: IntegrationServer.Router) -Process.unlink(pid) diff --git a/mix.exs b/mix.exs index 93e76f0..f80e113 100644 --- a/mix.exs +++ b/mix.exs @@ -47,6 +47,7 @@ defmodule Assent.MixProject do {:credo, "~> 1.1", only: [:dev, :test]}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:test_server, "~> 0.1.0", only: :test}, + {:plug, ">= 0.0.0", only: [:dev, :test]}, {:bandit, ">= 0.0.0", only: :test} ] end