Skip to content

Commit

Permalink
Added some more unit tests (#6)
Browse files Browse the repository at this point in the history
* Made some tests more async

* Got some good testing for the mutations

* Working test cases

* Refactoring

* Fixed a stupid bug

* Removed print statements

* Fixed some other things
  • Loading branch information
nvsneddon authored Sep 30, 2024
1 parent 2788964 commit 9ddd409
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 39 deletions.
2 changes: 1 addition & 1 deletion lib/perseus/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Perseus.Accounts.User do
user
|> cast(attrs, [:first_name, :last_name, :email])
|> validate_format(:email, ~r/@*\.[A-Za-z.]{2,}/, message: "Must be a valid email address")
|> unique_constraint(:email, name: :unique_email)
|> unique_constraint(:email)
|> validate_required([:first_name, :last_name, :email])
end
end
30 changes: 9 additions & 21 deletions lib/perseus/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,26 @@ defmodule Perseus.Auth do
with {:ok, _} <- Accounts.get_user_by_email(email),
{:ok, token} <- generate_login_token(email),
encoded_token <- BinaryUtils.encode(token) do
# If `url_fun` is provided, deliver the login link via email
if url_fun do
UserNotifier.deliver_login_link(email, url_fun.(encoded_token))
else
# Otherwise, just return the encoded token
{:ok, encoded_token}
end
else
_ -> :error
end
end

def send_signup_email(email, url_fun) do
with {:ok, token} <- generate_login_token(email),
encoded_token <- BinaryUtils.encode(token),
{:ok, _} <- UserNotifier.deliver_signup_link(email, url_fun.(encoded_token)) do
end
end

def login_user_by_login_token(token) do
with {:ok, email} <- find_email(:magic_link, token),
{:ok, session_token} <- generate_signup_token(email) do
{:ok, session_token}
def send_signup_email(email, url_fun \\ nil) do
with {:ok, token} <- generate_signup_token(email),
encoded_token <- BinaryUtils.encode(token) do
if url_fun do
UserNotifier.deliver_signup_link(email, url_fun.(encoded_token))
else
{:ok, encoded_token}
end
else
{:error, :expired_token} ->
{:error, "Expired token"}

{:error, :not_found} ->
{:error, "Not found"}

_ ->
{:error, "Token store Error"}
_ -> :error
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/perseus/auth/token_store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule Perseus.Auth.TokenStore do
end

def validate_signup_token(token) do
validate_token(token, @signup_table, true)
validate_token(token, @signup_table)
end

def validate_session_token(token) do
Expand Down
2 changes: 2 additions & 0 deletions lib/perseus_web/plugs/authenticate_user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule PerseusWeb.Plugs.AuthenticateUser do

defp build_context(conn) do
case get_auth_token(conn) do
{:ok, nil, nil} -> %{}
{:ok, token_type, token} -> build_context_from_token(token, token_type)
_ -> %{error: "Authorization header invalid"}
end
Expand Down Expand Up @@ -42,6 +43,7 @@ defmodule PerseusWeb.Plugs.AuthenticateUser do

defp get_auth_token(conn) do
case get_req_header(conn, "authorization") do
[] -> {:ok, nil, nil}
["Bearer " <> token] -> decode_token(:session, token)
["MagicLink " <> token] -> decode_token(:magic_link, token)
["Signup " <> token] -> decode_token(:signup, token)
Expand Down
10 changes: 8 additions & 2 deletions lib/perseus_web/resolvers/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ defmodule PerseusWeb.Resolvers.Auth do
end

def send_signup_link(_parent, %{email: email}, _resolution) do
Auth.send_signup_email(email, &"localhost:3000/signup/#{&1}")
{:ok, true}
case Accounts.get_user_by_email(email) do
{:error, _} ->
Auth.send_signup_email(email, &"localhost:3000/signup/#{&1}")
{:ok, true}

{:ok, _user} ->
{:ok, true}
end
end

def login_user(_parent, _args, %{context: %{email: email}}) do
Expand Down
2 changes: 1 addition & 1 deletion lib/perseus_web/schema/mutations/auth_mutations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule PerseusWeb.Schema.Mutations.AuthMutations do

field :sign_up, :sign_up_response do
arg :user, :user_input
middleware PerseusWeb.Middleware.RequireAuth, :magic_link
middleware PerseusWeb.Middleware.RequireAuth, :signup

resolve &Resolvers.Auth.signup_user/3
end
Expand Down
101 changes: 101 additions & 0 deletions test/perseus_web/plugs/authenticate_user_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
defmodule PerseusWeb.Plugs.AuthenticateUserTest do
use PerseusWeb.ConnCase, async: true
import Perseus.AccountsFixtures

alias Perseus.Utils.BinaryUtils
alias PerseusWeb.Plugs.AuthenticateUser

describe "session token" do
setup do
# Here you can mock Auth or BinaryUtils as needed
user = user_fixture()
{:ok, valid_token} = Perseus.Auth.login_user_by_email(user.email)
%{user: user, token: BinaryUtils.encode(valid_token)}
end

test "adds context with user for valid session token", %{conn: conn, token: token, user: user} do
conn =
conn
|> put_req_header("authorization", "Bearer #{token}")
|> AuthenticateUser.call(%{})

# Verify that the context was added correctly
context = conn.private.absinthe.context
assert context[:user] == user
assert context[:token_type] == :session
end

test "returns error for invalid authorization header", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "Bearer invalidtoken}")
|> AuthenticateUser.call(%{})

# Check that an error is returned
context = conn.private.absinthe.context
assert context[:error] == "Authorization header invalid"
end
end

describe "login token" do
setup do
user = user_fixture()
{:ok, token} = Perseus.Auth.send_login_email(user.email)
%{user: user, token: token}
end

test "adds context with email for valid login token", %{conn: conn, user: user, token: token} do
conn =
conn
|> put_req_header("authorization", "MagicLink #{token}")
|> AuthenticateUser.call(%{})

# Verify that the context was added correctly
context = conn.private.absinthe.context
assert context[:email] == user.email
assert context[:token_type] == :magic_link
end
end

describe "signup token" do
setup do
email = unique_user_email()
{:ok, token} = Perseus.Auth.send_signup_email(email)
%{email: email, token: token}
end

test "adds context with email for valid signup token", %{
conn: conn,
email: email,
token: token
} do
conn =
conn
|> put_req_header("authorization", "Signup #{token}")
|> AuthenticateUser.call(%{})

# Verify that the context was added correctly
context = conn.private.absinthe.context
assert context[:email] == email
assert context[:token_type] == :signup
end
end

test "returns normal conn when no authorization header is present", %{conn: conn} do
conn = AuthenticateUser.call(conn, %{})

context = conn.private.absinthe.context
assert context == %{}
end

test "returns error for invalid authorization header", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "Invalid token}")
|> AuthenticateUser.call(%{})

# Check that an error is returned
context = conn.private.absinthe.context
assert context[:error] == "Authorization header invalid"
end
end
2 changes: 1 addition & 1 deletion test/perseus_web/resolvers/auth_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule PerseusWeb.Resolvers.AuthTest do
use Perseus.DataCase
use Perseus.DataCase, async: true

alias Perseus.Auth
alias PerseusWeb.Resolvers
Expand Down
124 changes: 113 additions & 11 deletions test/perseus_web/schema/mutations/auth_mutations_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@ defmodule PerseusWeb.Schema.Mutations.AuthMutationsTest do
import Perseus.AccountsFixtures

# Define the query that you will be testing
@login_query """
mutation startSession {
logIn {
sessionToken
}
}
"""

describe "login mutation" do
setup do
query = """
mutation startSession {
logIn {
sessionToken
}
}
"""

user = user_fixture()
{:ok, token} = Auth.send_login_email(user.email)
%{user: user, token: token}
%{user: user, token: token, query: query}
end

test "should login when email is found", %{conn: conn, user: user, token: token} do
test "should login when email is found", %{conn: conn, user: user, token: token, query: query} do
conn =
conn
|> put_req_header("authorization", "MagicLink " <> token)
|> put_req_header("x-custom-header", "custom-value")
|> post("/api/graphql", %{query: @login_query})
|> post("/api/graphql", %{query: query})

assert json_response(conn, 200)
response_data = json_response(conn, 200)["data"]
Expand All @@ -40,5 +40,107 @@ defmodule PerseusWeb.Schema.Mutations.AuthMutationsTest do
{:ok, decoded_session_token} = BinaryUtils.decode(session_token)
assert {:ok, ^user} = Auth.find_user(decoded_session_token)
end

test "should not resolve if token is misconfigured", %{conn: conn, query: query} do
conn =
conn
|> put_req_header("authorization", "MagicLink " <> "invalidtoken")
|> post("/api/graphql", %{query: query})

assert json_response(conn, 200)
errors = json_response(conn, 200)["errors"]

assert [%{"code" => "UNAUTHENTICATED", "message" => "not_found"}] = errors
end

test "should only allow magic link tokens", %{conn: conn, query: query, token: token} do
conn =
conn
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/graphql", %{query: query})

assert json_response(conn, 200)
errors = json_response(conn, 200)["errors"]

assert [%{"code" => "UNAUTHENTICATED", "message" => "not_found"}] = errors
end
end

describe "sendLoginLink mutation" do
import Swoosh.TestAssertions

setup do
query = """
mutation($email: String!) {
sendLoginLink(email: $email)
}
"""

user = user_fixture()
%{query: query, user: user}
end

test "should send an email when email is found and return true", %{
conn: conn,
query: query,
user: user
} do
variables = %{"email" => user.email}
conn = post(conn, "/api/graphql", %{query: query, variables: variables})

assert json_response(conn, 200)
response_data = json_response(conn, 200)["data"]

assert %{"sendLoginLink" => true} = response_data

assert_email_sent(fn email ->
[{_, delivered_to}] = email.to
assert delivered_to == user.email
end)
end

test "should not send an email when email is found and still return true", %{
conn: conn,
query: query
} do
variables = %{"email" => "invalid@example.com"}
conn = post(conn, "/api/graphql", %{query: query, variables: variables})

assert json_response(conn, 200)
response_data = json_response(conn, 200)["data"]

assert %{"sendLoginLink" => true} = response_data
assert_no_email_sent()
end
end

describe "sendSignupLink mutation" do
import Swoosh.TestAssertions

setup do
query = """
mutation ($email: String!) {
sendSignupLink(email: $email)
}
"""

%{query: query}
end

test "should send an email to sign up", %{conn: conn, query: query} do
email_address = "new.email@example.com"
variables = %{"email" => email_address}
conn = post(conn, "/api/graphql", %{query: query, variables: variables})

assert json_response(conn, 200)
response_data = json_response(conn, 200)["data"]

assert %{"sendSignupLink" => true} = response_data

assert_email_sent(fn email ->
[{_, delivered_to}] = email.to
assert delivered_to == email_address
end)
end
end
end
4 changes: 3 additions & 1 deletion test/support/fixtures/accounts_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Perseus.AccountsFixtures do
entities via the `Perseus.Accounts` context.
"""

def unique_user_email, do: "user#{System.unique_integer()}@example.com"

@doc """
Generate a user.
"""
Expand All @@ -18,7 +20,7 @@ defmodule Perseus.AccountsFixtures do

def valid_user_attributes(attr \\ %{}) do
Enum.into(attr, %{
email: "someemail@example.com",
email: unique_user_email(),
first_name: "some first_name",
last_name: "some last_name",
verified: true
Expand Down

0 comments on commit 9ddd409

Please sign in to comment.