Skip to content
This repository has been archived by the owner on Nov 24, 2020. It is now read-only.

Implement mox #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 9 additions & 42 deletions lib/spacesuit/session_service.ex
Original file line number Diff line number Diff line change
@@ -1,36 +1,3 @@
defmodule SessionService do
@moduledoc """
Describe a SessionService implementation. Use for handling auth derived
from bearer tokens.
"""

@callback validate_api_token(String.t()) :: Tuple.t()
@callback handle_bearer_token(Map.t(), Map.t(), String.t(), String.t()) :: Tuple.t()
end

defmodule Spacesuit.MockSessionService do
@behaviour SessionService
@moduledoc """
Mock session service used in testing the AuthHandler
"""

def validate_api_token(token) do
case token do
"ok" -> :ok
"error" -> :error
_ -> :error
end
end

def handle_bearer_token(req, env, token, _url) do
case token do
"ok" -> {:ok, req, env}
"error" -> {:stop, req}
_ -> {:ok, req, env}
end
end
end

defmodule Spacesuit.SessionService do
@moduledoc """
Implementation of a SessionService that calls out to an external service
Expand All @@ -40,7 +7,8 @@ defmodule Spacesuit.SessionService do
require Logger
use Elixometer

@behaviour SessionService
@callback validate_api_token(String.t()) :: Tuple.t()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the only reason for a behaviour is for testing, it's pretty common (and acceptable) to move the callbacks/behaviour definition into the module that implements it in prod.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super useful because I didn't know about Mox and so I'll definitely check it out. I'm a little lost about how it works ATM moment but I'm sure some reading will clear it up.

In this case we only had one implementation and a mock, but this module is not that generic and the idea was that people could provide their own implementation of a SessionService if they needed to handle it differently. Given that I'd like to support that, but that also nobody else seems to be interested, I'm on the fence about merging.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, not a problem. I will update the PR to leave the callback in the SessionService behaviour. That is technically a cleaner implementation and definitely the way to go if you want the interface behaviour for more than just testing. Mox will work just fine that way, too.

@callback handle_bearer_token(Map.t(), Map.t(), String.t(), String.t()) :: Tuple.t()

@http_server Application.get_env(:spacesuit, :http_server)
# How many milliseconds before we timeout call to session-service
Expand All @@ -63,15 +31,13 @@ defmodule Spacesuit.SessionService do
{:ok, _, _} ->
result

{:error, type, code, error} ->
{:error, type, code, error} when is_binary(error) ->
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the guard clause allows us to treat this as a separate case instead of nesting flow control.

Logger.error("Session-service #{inspect(type)} error: #{inspect(error)}")
@http_server.reply(code, %{}, error, req)
{:stop, req}

if is_binary(error) do
@http_server.reply(code, %{}, error, req)
else
error_reply(req, 503, "Upstream error")
end

{:error, _type, _code, _error} ->
error_reply(req, 503, "Upstream error")
{:stop, req}

{:error, type, error} ->
Expand Down Expand Up @@ -139,7 +105,8 @@ defmodule Spacesuit.SessionService do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, body}

{:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 400 and code <= 499 ->
{:ok, %HTTPoison.Response{status_code: code, body: body}}
when code >= 400 and code <= 499 ->
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry. This is from the 1.6.4 formatter. I can remove it if you want, but I recommend updating and starting to use the formatter.

{:error, :http, code, body}

{:error, %HTTPoison.Error{reason: reason}} ->
Expand Down
13 changes: 3 additions & 10 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,8 @@ defmodule Spacesuit.Mixfile do
# Type "mix help compile.app" for more information
def application do
[
applications: [
:logger,
:cowboy,
:hackney,
:crypto,
:jose,
:exometer_newrelic_reporter,
:elixometer
],
mod: {Spacesuit, []}
mod: {Spacesuit, []},
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of 1.4(?) you no longer want to explicitly pass applications that need to be started. While this kind of sucks since it takes away the explicitness, it means that you can't mess up by forgetting to add a new application to the list when you start using it. In my case, I needed to clean this up or mox wasn't getting started.

extra_applications: []
]
end

Expand Down Expand Up @@ -62,6 +54,7 @@ defmodule Spacesuit.Mixfile do
# Test only
{:excoveralls, "~> 0.6", only: :test},
{:mock, "~> 0.1.1", only: :test},
{:mox, "~> 0.3.2", only: :test},
{:apex, "~>1.1.0"}
]
end
Expand Down
3 changes: 2 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"mock": {:hex, :mock, "0.1.3", "657937b03f88fce89b3f7d6becc9f1ec1ac19c71081aeb32117db9bc4d9b3980", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]},
"mox": {:hex, :mox, "0.3.2", "3b9b8364fd4f28628139de701d97c636b27a8f925f57a8d5a1b85fbd620dad3a", [:mix], [], "hexpm"},
"netlink": {:git, "git://github.com/Feuerlabs/netlink.git", "aab67b0a16d88e32a2790a3c0a06c37f98ae121e", [ref: "aab67b0"]},
"parse_trans": {:hex, :parse_trans, "2.9.0", "3f5f7b402928fb9fd200c891e635de909045d1efac40ce3f924d3892898f85eb", [:rebar], [{:edown, "> 0.0.0", [hex: :edown, optional: false]}]},
"pobox": {:hex, :pobox, "1.0.2", "45a5bc91e3cf20f9bc0b94494f00fcdccbc333ab2e0856972b7f0f196fc41613", [:rebar3], []},
"poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []},
"rabbit_common": {:git, "git://github.com/jbrisbin/rabbit_common.git", "9c1965273032ffb79ec0ff80b250e5d0b4608aa7", [tag: "rabbitmq-3.3.5"]},
"ranch": {:git, "https://github.com/ninenines/ranch", "40809cd2b257a8a53bcb5588ecaa88cc5381ff5c", [ref: "1.3.0"]},
"setup": {:hex, :setup, "1.8.4", "738db0685dc1741f45c6a9bf78478e0d5877f3d0876c0b50fd02f0210edb5aa4", [:rebar3], []},
"setup": {:hex, :setup, "1.8.4", "738db0685dc1741f45c6a9bf78478e0d5877f3d0876c0b50fd02f0210edb5aa4", [:rebar], []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []},
}
122 changes: 99 additions & 23 deletions test/spacesuit_auth_middleware_test.exs
Original file line number Diff line number Diff line change
@@ -1,91 +1,167 @@
defmodule SpacesuitAuthMiddlewareTest do
use ExUnit.Case
import Mox
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

importing gives us mock and stub as local functions. If you prefer it to be more explicit, you can require Mox and the calls will be Mox.mock and Mox.stub.


doctest Spacesuit.AuthMiddleware

setup_all do
token = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJhY2N0IjoiMSIsImF6cCI6ImthcmwubWF0dGhpYXNAZ29uaXRyby5jb20iLCJkZWxlZ2F0ZSI6IiIsImV4cCI6IjIwMTctMDItMDNUMTU6MDc6MTRaIiwiZmVhdHVyZXMiOlsidGVhbWRvY3MiLCJjb21iaW5lIiwiZXNpZ24iXSwiaWF0IjoiMjAxNy0wMi0wM1QxNDowNzoxNC40MTMyMTg2OTNaIiwianRpIjoiNTU2ZmU1MTgtYTk0Mi00YTQ3LTkyZmMtNWNmNmVkOWY0YWFhIiwicGVybXMiOlsiYWNjb3VudHM6cmVhZCIsImdyb3VwczpyZWFkIiwidXNlcnM6d3JpdGUiXSwic3ViIjoiY3NzcGVyc29uQGdvbml0cm8uY29tIn0.6eWCzu6yHhgzuvUPaNloNl09uUfaN6nqhK1W--TQwtMk29tf5C5SV-hTT2pxnSxe"
token =
"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJhY2N0IjoiMSIsImF6cCI6ImthcmwubWF0dGhpYXNAZ29uaXRyby5jb20iLCJkZWxlZ2F0ZSI6IiIsImV4cCI6IjIwMTctMDItMDNUMTU6MDc6MTRaIiwiZmVhdHVyZXMiOlsidGVhbWRvY3MiLCJjb21iaW5lIiwiZXNpZ24iXSwiaWF0IjoiMjAxNy0wMi0wM1QxNDowNzoxNC40MTMyMTg2OTNaIiwianRpIjoiNTU2ZmU1MTgtYTk0Mi00YTQ3LTkyZmMtNWNmNmVkOWY0YWFhIiwicGVybXMiOlsiYWNjb3VudHM6cmVhZCIsImdyb3VwczpyZWFkIiwidXNlcnM6d3JpdGUiXSwic3ViIjoiY3NzcGVyc29uQGdvbml0cm8uY29tIn0.6eWCzu6yHhgzuvUPaNloNl09uUfaN6nqhK1W--TQwtMk29tf5C5SV-hTT2pxnSxe"

{:ok, token: token}
end

describe "handling non-bearer tokens" do
test "passes through OK when there is no auth header" do
assert {:ok, %{}, %{}} = Spacesuit.AuthMiddleware.execute(%{}, %{})
assert {:ok, %{}, %{}} = Spacesuit.AuthMiddleware.execute(%{}, %{})
end

test "'authorization' header is stripped when present" do
req = %{ headers: %{ "authorization" => "sometoken" }}
req = %{headers: %{"authorization" => "sometoken"}}
env = %{}

assert {:ok, %{ headers: %{} }, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
assert {:ok, %{headers: %{}}, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end
end

describe "handling bearer tokens" do
test "with a valid token", state do
req = %{ headers: %{ "authorization" => "Bearer #{state[:token]}" }, pid: self(), streamid: 1, method: "GET" }
test "with a valid token", %{token: token} do
req = %{
headers: %{"authorization" => "Bearer #{token}"},
pid: self(),
streamid: 1,
method: "GET"
}

env = %{}

assert {:ok, %{ headers: _headers }, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
stub(MockSessionService, :handle_bearer_token, fn req, env, ^token, _url ->
{:ok, req, env}
end)

assert {:ok, %{headers: _headers}, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with invalid bearer token and without session service" do
Application.put_env(:spacesuit, :session_service, %{ enabled: false })
req = %{ headers: %{ "authorization" => "Bearer balloney" }, pid: self(), streamid: 1, method: "GET" }
Application.put_env(:spacesuit, :session_service, %{enabled: false})

req = %{
headers: %{"authorization" => "Bearer balloney"},
pid: self(),
streamid: 1,
method: "GET"
}

env = %{}

stub(MockSessionService, :handle_bearer_token, fn req, env, _token, _url ->
{:ok, req, env}
end)

# Should just pass through unaffected
assert {:ok, ^req, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with an invalid token when session service is enabled" do
Application.put_env(:spacesuit, :session_service, %{ enabled: true, impl: Spacesuit.MockSessionService })
Application.put_env(:spacesuit, :session_service, %{enabled: true, impl: MockSessionService})

bad_token = "imabadtoken"

req = %{
headers: %{"authorization" => "Bearer #{bad_token}"},
pid: self(),
streamid: 1,
method: "GET"
}

req = %{ headers: %{ "authorization" => "Bearer error" }, pid: self(), streamid: 1, method: "GET" }
env = %{}

stub(MockSessionService, :handle_bearer_token, fn req, _env, ^bad_token, _url ->
{:stop, req}
end)

# Unrecognized, we pass it on as is
assert {:stop, ^req} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with a valid token when session service is enabled" do
Application.put_env(:spacesuit, :session_service, %{ enabled: true, impl: Spacesuit.MockSessionService })
test "with a valid token when session service is enabled", %{token: token} do
Application.put_env(:spacesuit, :session_service, %{enabled: true, impl: MockSessionService})

stub(MockSessionService, :handle_bearer_token, fn req, env, ^token, _url ->
{:ok, req, env}
end)

req = %{
headers: %{"authorization" => "Bearer #{token}"},
pid: self(),
streamid: 1,
method: "GET"
}

req = %{ headers: %{ "authorization" => "Bearer ok" }, pid: self(), streamid: 1, method: "GET" }
env = %{}

# Unrecognized, we pass it on as is
assert {:ok, ^req, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with a missing token when session service is enabled" do
Application.put_env(:spacesuit, :session_service, %{ enabled: true, impl: Spacesuit.MockSessionService })
Application.put_env(:spacesuit, :session_service, %{enabled: true, impl: MockSessionService})

empty_token = ""

stub(MockSessionService, :handle_bearer_token, fn req, env, ^empty_token, _url ->
{:ok, req, env}
end)

req = %{ headers: %{ "authorization" => "Bearer " }, pid: self(), streamid: 1, method: "GET" }
req = %{headers: %{"authorization" => "Bearer "}, pid: self(), streamid: 1, method: "GET"}
env = %{}

# Unrecognized, we pass it on as is
assert {:ok, ^req, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with a valid token on a bypassed path" do
Application.put_env(:spacesuit, :session_service, %{ enabled: true, impl: Spacesuit.MockSessionService })
Application.put_env(:handler_opts, :middleware, %{ session_service: :disabled })
test "with a valid token on a bypassed path", %{token: token} do
Application.put_env(:spacesuit, :session_service, %{enabled: true, impl: MockSessionService})

Application.put_env(:handler_opts, :middleware, %{session_service: :disabled})

req = %{
headers: %{"authorization" => "Bearer #{token}"},
pid: self(),
streamid: 1,
method: "GET"
}

req = %{ headers: %{ "authorization" => "Bearer ok" }, pid: self(), streamid: 1, method: "GET" }
env = %{}

stub(MockSessionService, :handle_bearer_token, fn req, env, ^token, _url ->
{:ok, req, env}
end)

# pass it on as is
assert {:ok, ^req, ^env} = Spacesuit.AuthMiddleware.execute(req, env)
end

test "with an invalid token on a bypassed path" do
Application.put_env(:spacesuit, :session_service, %{ enabled: true, impl: Spacesuit.MockSessionService })
Application.put_env(:handler_opts, :middleware, %{ session_service: :disabled })
Application.put_env(:spacesuit, :session_service, %{enabled: true, impl: MockSessionService})

Application.put_env(:handler_opts, :middleware, %{session_service: :disabled})

bad_token = "iamabadtoken"

req = %{
headers: %{"authorization" => "Bearer #{bad_token}"},
pid: self(),
streamid: 1,
method: "GET"
}

req = %{ headers: %{ "authorization" => "Bearer error" }, pid: self(), streamid: 1, method: "GET" }
env = %{}

stub(MockSessionService, :handle_bearer_token, fn req, _env, ^bad_token, _url ->
{:stop, req}
end)

# Unrecognized, we pass it on as is
assert {:stop, ^req} = Spacesuit.AuthMiddleware.execute(req, env)
end
Expand Down
2 changes: 2 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
ExUnit.start()

Mox.defmock(MockSessionService, for: Spacesuit.SessionService)