lecutre 07
This commit is contained in:
122 files changed
+9723
No files matched your search
@@ -0,0 +1,397 @@
|
||||
defmodule Bcit.AccountsTest do
|
||||
use Bcit.DataCase
|
||||
|
||||
alias Bcit.Accounts
|
||||
|
||||
import Bcit.AccountsFixtures
|
||||
alias Bcit.Accounts.{User, UserToken}
|
||||
|
||||
describe "get_user_by_email/1" do
|
||||
test "does not return the user if the email does not exist" do
|
||||
refute Accounts.get_user_by_email("unknown@example.com")
|
||||
end
|
||||
|
||||
test "returns the user if the email exists" do
|
||||
%{id: id} = user = user_fixture()
|
||||
assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_user_by_email_and_password/2" do
|
||||
test "does not return the user if the email does not exist" do
|
||||
refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
|
||||
end
|
||||
|
||||
test "does not return the user if the password is not valid" do
|
||||
user = user_fixture() |> set_password()
|
||||
refute Accounts.get_user_by_email_and_password(user.email, "invalid")
|
||||
end
|
||||
|
||||
test "returns the user if the email and password are valid" do
|
||||
%{id: id} = user = user_fixture() |> set_password()
|
||||
|
||||
assert %User{id: ^id} =
|
||||
Accounts.get_user_by_email_and_password(user.email, valid_user_password())
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_user!/1" do
|
||||
test "raises if id is invalid" do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Accounts.get_user!(-1)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns the user with the given id" do
|
||||
%{id: id} = user = user_fixture()
|
||||
assert %User{id: ^id} = Accounts.get_user!(user.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "register_user/1" do
|
||||
test "requires email to be set" do
|
||||
{:error, changeset} = Accounts.register_user(%{})
|
||||
|
||||
assert %{email: ["can't be blank"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "validates email when given" do
|
||||
{:error, changeset} = Accounts.register_user(%{email: "not valid"})
|
||||
|
||||
assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "validates maximum values for email for security" do
|
||||
too_long = String.duplicate("db", 100)
|
||||
{:error, changeset} = Accounts.register_user(%{email: too_long})
|
||||
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
||||
end
|
||||
|
||||
test "validates email uniqueness" do
|
||||
%{email: email} = user_fixture()
|
||||
{:error, changeset} = Accounts.register_user(%{email: email})
|
||||
assert "has already been taken" in errors_on(changeset).email
|
||||
|
||||
# Now try with the uppercased email too, to check that email case is ignored.
|
||||
{:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
|
||||
assert "has already been taken" in errors_on(changeset).email
|
||||
end
|
||||
|
||||
test "registers users without password" do
|
||||
email = unique_user_email()
|
||||
{:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
|
||||
assert user.email == email
|
||||
assert is_nil(user.hashed_password)
|
||||
assert is_nil(user.confirmed_at)
|
||||
assert is_nil(user.password)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sudo_mode?/2" do
|
||||
test "validates the authenticated_at time" do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()})
|
||||
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)})
|
||||
refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)})
|
||||
|
||||
# minute override
|
||||
refute Accounts.sudo_mode?(
|
||||
%User{authenticated_at: DateTime.add(now, -11, :minute)},
|
||||
-10
|
||||
)
|
||||
|
||||
# not authenticated
|
||||
refute Accounts.sudo_mode?(%User{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_user_email/3" do
|
||||
test "returns a user changeset" do
|
||||
assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
|
||||
assert changeset.required == [:email]
|
||||
end
|
||||
end
|
||||
|
||||
describe "deliver_user_update_email_instructions/3" do
|
||||
setup do
|
||||
%{user: user_fixture()}
|
||||
end
|
||||
|
||||
test "sends token through notification", %{user: user} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
|
||||
end)
|
||||
|
||||
{:ok, token} = Base.url_decode64(token, padding: false)
|
||||
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
||||
assert user_token.user_id == user.id
|
||||
assert user_token.sent_to == user.email
|
||||
assert user_token.context == "change:current@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_user_email/2" do
|
||||
setup do
|
||||
user = unconfirmed_user_fixture()
|
||||
email = unique_user_email()
|
||||
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
||||
end)
|
||||
|
||||
%{user: user, token: token, email: email}
|
||||
end
|
||||
|
||||
test "updates the email with a valid token", %{user: user, token: token, email: email} do
|
||||
assert {:ok, %{email: ^email}} = Accounts.update_user_email(user, token)
|
||||
changed_user = Repo.get!(User, user.id)
|
||||
assert changed_user.email != user.email
|
||||
assert changed_user.email == email
|
||||
refute Repo.get_by(UserToken, user_id: user.id)
|
||||
end
|
||||
|
||||
test "does not update email with invalid token", %{user: user} do
|
||||
assert Accounts.update_user_email(user, "oops") ==
|
||||
{:error, :transaction_aborted}
|
||||
|
||||
assert Repo.get!(User, user.id).email == user.email
|
||||
assert Repo.get_by(UserToken, user_id: user.id)
|
||||
end
|
||||
|
||||
test "does not update email if user email changed", %{user: user, token: token} do
|
||||
assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) ==
|
||||
{:error, :transaction_aborted}
|
||||
|
||||
assert Repo.get!(User, user.id).email == user.email
|
||||
assert Repo.get_by(UserToken, user_id: user.id)
|
||||
end
|
||||
|
||||
test "does not update email if token expired", %{user: user, token: token} do
|
||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||
|
||||
assert Accounts.update_user_email(user, token) ==
|
||||
{:error, :transaction_aborted}
|
||||
|
||||
assert Repo.get!(User, user.id).email == user.email
|
||||
assert Repo.get_by(UserToken, user_id: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_user_password/3" do
|
||||
test "returns a user changeset" do
|
||||
assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
|
||||
assert changeset.required == [:password]
|
||||
end
|
||||
|
||||
test "allows fields to be set" do
|
||||
changeset =
|
||||
Accounts.change_user_password(
|
||||
%User{},
|
||||
%{
|
||||
"password" => "new valid password"
|
||||
},
|
||||
hash_password: false
|
||||
)
|
||||
|
||||
assert changeset.valid?
|
||||
assert get_change(changeset, :password) == "new valid password"
|
||||
assert is_nil(get_change(changeset, :hashed_password))
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_user_password/2" do
|
||||
setup do
|
||||
%{user: user_fixture()}
|
||||
end
|
||||
|
||||
test "validates password", %{user: user} do
|
||||
{:error, changeset} =
|
||||
Accounts.update_user_password(user, %{
|
||||
password: "not valid",
|
||||
password_confirmation: "another"
|
||||
})
|
||||
|
||||
assert %{
|
||||
password: ["should be at least 12 character(s)"],
|
||||
password_confirmation: ["does not match password"]
|
||||
} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "validates maximum values for password for security", %{user: user} do
|
||||
too_long = String.duplicate("db", 100)
|
||||
|
||||
{:error, changeset} =
|
||||
Accounts.update_user_password(user, %{password: too_long})
|
||||
|
||||
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
||||
end
|
||||
|
||||
test "updates the password", %{user: user} do
|
||||
{:ok, {user, expired_tokens}} =
|
||||
Accounts.update_user_password(user, %{
|
||||
password: "new valid password"
|
||||
})
|
||||
|
||||
assert expired_tokens == []
|
||||
assert is_nil(user.password)
|
||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
||||
end
|
||||
|
||||
test "deletes all tokens for the given user", %{user: user} do
|
||||
_ = Accounts.generate_user_session_token(user)
|
||||
|
||||
{:ok, {_, _}} =
|
||||
Accounts.update_user_password(user, %{
|
||||
password: "new valid password"
|
||||
})
|
||||
|
||||
refute Repo.get_by(UserToken, user_id: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "generate_user_session_token/1" do
|
||||
setup do
|
||||
%{user: user_fixture()}
|
||||
end
|
||||
|
||||
test "generates a token", %{user: user} do
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
assert user_token = Repo.get_by(UserToken, token: token)
|
||||
assert user_token.context == "session"
|
||||
assert user_token.authenticated_at != nil
|
||||
|
||||
# Creating the same token for another user should fail
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
Repo.insert!(%UserToken{
|
||||
token: user_token.token,
|
||||
user_id: user_fixture().id,
|
||||
context: "session"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
test "duplicates the authenticated_at of given user in new token", %{user: user} do
|
||||
user = %{user | authenticated_at: DateTime.add(DateTime.utc_now(:second), -3600)}
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
assert user_token = Repo.get_by(UserToken, token: token)
|
||||
assert user_token.authenticated_at == user.authenticated_at
|
||||
assert DateTime.compare(user_token.inserted_at, user.authenticated_at) == :gt
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_user_by_session_token/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
%{user: user, token: token}
|
||||
end
|
||||
|
||||
test "returns user by token", %{user: user, token: token} do
|
||||
assert {session_user, token_inserted_at} = Accounts.get_user_by_session_token(token)
|
||||
assert session_user.id == user.id
|
||||
assert session_user.authenticated_at != nil
|
||||
assert token_inserted_at != nil
|
||||
end
|
||||
|
||||
test "does not return user for invalid token" do
|
||||
refute Accounts.get_user_by_session_token("oops")
|
||||
end
|
||||
|
||||
test "does not return user for expired token", %{token: token} do
|
||||
dt = ~N[2020-01-01 00:00:00]
|
||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: dt, authenticated_at: dt])
|
||||
refute Accounts.get_user_by_session_token(token)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_user_by_magic_link_token/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{encoded_token, _hashed_token} = generate_user_magic_link_token(user)
|
||||
%{user: user, token: encoded_token}
|
||||
end
|
||||
|
||||
test "returns user by token", %{user: user, token: token} do
|
||||
assert session_user = Accounts.get_user_by_magic_link_token(token)
|
||||
assert session_user.id == user.id
|
||||
end
|
||||
|
||||
test "does not return user for invalid token" do
|
||||
refute Accounts.get_user_by_magic_link_token("oops")
|
||||
end
|
||||
|
||||
test "does not return user for expired token", %{token: token} do
|
||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||
refute Accounts.get_user_by_magic_link_token(token)
|
||||
end
|
||||
end
|
||||
|
||||
describe "login_user_by_magic_link/1" do
|
||||
test "confirms user and expires tokens" do
|
||||
user = unconfirmed_user_fixture()
|
||||
refute user.confirmed_at
|
||||
{encoded_token, hashed_token} = generate_user_magic_link_token(user)
|
||||
|
||||
assert {:ok, {user, [%{token: ^hashed_token}]}} =
|
||||
Accounts.login_user_by_magic_link(encoded_token)
|
||||
|
||||
assert user.confirmed_at
|
||||
end
|
||||
|
||||
test "returns user and (deleted) token for confirmed user" do
|
||||
user = user_fixture()
|
||||
assert user.confirmed_at
|
||||
{encoded_token, _hashed_token} = generate_user_magic_link_token(user)
|
||||
assert {:ok, {^user, []}} = Accounts.login_user_by_magic_link(encoded_token)
|
||||
# one time use only
|
||||
assert {:error, :not_found} = Accounts.login_user_by_magic_link(encoded_token)
|
||||
end
|
||||
|
||||
test "raises when unconfirmed user has password set" do
|
||||
user = unconfirmed_user_fixture()
|
||||
{1, nil} = Repo.update_all(User, set: [hashed_password: "hashed"])
|
||||
{encoded_token, _hashed_token} = generate_user_magic_link_token(user)
|
||||
|
||||
assert_raise RuntimeError, ~r/magic link log in is not allowed/, fn ->
|
||||
Accounts.login_user_by_magic_link(encoded_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_user_session_token/1" do
|
||||
test "deletes the token" do
|
||||
user = user_fixture()
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
assert Accounts.delete_user_session_token(token) == :ok
|
||||
refute Accounts.get_user_by_session_token(token)
|
||||
end
|
||||
end
|
||||
|
||||
describe "deliver_login_instructions/2" do
|
||||
setup do
|
||||
%{user: unconfirmed_user_fixture()}
|
||||
end
|
||||
|
||||
test "sends token through notification", %{user: user} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, token} = Base.url_decode64(token, padding: false)
|
||||
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
||||
assert user_token.user_id == user.id
|
||||
assert user_token.sent_to == user.email
|
||||
assert user_token.context == "login"
|
||||
end
|
||||
end
|
||||
|
||||
describe "inspect/2 for the User module" do
|
||||
test "does not include password" do
|
||||
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
defmodule Bcit.SchoolsTest do
|
||||
use Bcit.DataCase
|
||||
|
||||
alias Bcit.Schools
|
||||
|
||||
describe "students" do
|
||||
alias Bcit.Schools.Student
|
||||
|
||||
import Bcit.SchoolsFixtures
|
||||
|
||||
@invalid_attrs %{sid: nil, firstname: nil, lastname: nil, gpa: nil}
|
||||
|
||||
test "list_students/0 returns all students" do
|
||||
student = student_fixture()
|
||||
assert Schools.list_students() == [student]
|
||||
end
|
||||
|
||||
test "get_student!/1 returns the student with given id" do
|
||||
student = student_fixture()
|
||||
assert Schools.get_student!(student.id) == student
|
||||
end
|
||||
|
||||
test "create_student/1 with valid data creates a student" do
|
||||
valid_attrs = %{
|
||||
sid: "A00000001",
|
||||
firstname: "some firstname",
|
||||
lastname: "some lastname",
|
||||
gpa: 85.5
|
||||
}
|
||||
|
||||
assert {:ok, %Student{} = student} = Schools.create_student(valid_attrs)
|
||||
assert student.sid == "A00000001"
|
||||
assert student.firstname == "some firstname"
|
||||
assert student.lastname == "some lastname"
|
||||
assert student.gpa == 85.5
|
||||
end
|
||||
|
||||
test "create_student/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Schools.create_student(@invalid_attrs)
|
||||
end
|
||||
|
||||
test "update_student/2 with valid data updates the student" do
|
||||
student = student_fixture()
|
||||
|
||||
update_attrs = %{
|
||||
sid: "A00000002",
|
||||
firstname: "some updated firstname",
|
||||
lastname: "some updated lastname",
|
||||
gpa: 92.3
|
||||
}
|
||||
|
||||
assert {:ok, %Student{} = student} = Schools.update_student(student, update_attrs)
|
||||
assert student.sid == "A00000002"
|
||||
assert student.firstname == "some updated firstname"
|
||||
assert student.lastname == "some updated lastname"
|
||||
assert student.gpa == 92.3
|
||||
end
|
||||
|
||||
test "update_student/2 with invalid data returns error changeset" do
|
||||
student = student_fixture()
|
||||
assert {:error, %Ecto.Changeset{}} = Schools.update_student(student, @invalid_attrs)
|
||||
assert student == Schools.get_student!(student.id)
|
||||
end
|
||||
|
||||
test "delete_student/1 deletes the student" do
|
||||
student = student_fixture()
|
||||
assert {:ok, %Student{}} = Schools.delete_student(student)
|
||||
assert_raise Ecto.NoResultsError, fn -> Schools.get_student!(student.id) end
|
||||
end
|
||||
|
||||
test "change_student/1 returns a student changeset" do
|
||||
student = student_fixture()
|
||||
assert %Ecto.Changeset{} = Schools.change_student(student)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
defmodule BcitWeb.ErrorHTMLTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
# Bring render_to_string/4 for testing custom views
|
||||
import Phoenix.Template, only: [render_to_string: 4]
|
||||
|
||||
test "renders 404.html" do
|
||||
assert render_to_string(BcitWeb.ErrorHTML, "404", "html", []) == "Not Found"
|
||||
end
|
||||
|
||||
test "renders 500.html" do
|
||||
assert render_to_string(BcitWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
defmodule BcitWeb.ErrorJSONTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
test "renders 404" do
|
||||
assert BcitWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
||||
end
|
||||
|
||||
test "renders 500" do
|
||||
assert BcitWeb.ErrorJSON.render("500.json", %{}) ==
|
||||
%{errors: %{detail: "Internal Server Error"}}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
defmodule BcitWeb.PageControllerTest do
|
||||
use BcitWeb.ConnCase
|
||||
|
||||
test "GET /", %{conn: conn} do
|
||||
conn = get(conn, ~p"/")
|
||||
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
defmodule BcitWeb.StudentControllerTest do
|
||||
use BcitWeb.ConnCase
|
||||
|
||||
import Bcit.SchoolsFixtures
|
||||
|
||||
@create_attrs %{
|
||||
sid: "A00000001",
|
||||
firstname: "some firstname",
|
||||
lastname: "some lastname",
|
||||
gpa: 85.5
|
||||
}
|
||||
@update_attrs %{
|
||||
sid: "A00000002",
|
||||
firstname: "some updated firstname",
|
||||
lastname: "some updated lastname",
|
||||
gpa: 92.3
|
||||
}
|
||||
@invalid_attrs %{sid: nil, firstname: nil, lastname: nil, gpa: nil}
|
||||
|
||||
describe "index" do
|
||||
test "lists all students", %{conn: conn} do
|
||||
conn = get(conn, ~p"/students")
|
||||
assert html_response(conn, 200) =~ "Listing Students"
|
||||
end
|
||||
end
|
||||
|
||||
describe "new student" do
|
||||
test "renders form", %{conn: conn} do
|
||||
conn = get(conn, ~p"/students/new")
|
||||
assert html_response(conn, 200) =~ "New Student"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create student" do
|
||||
test "redirects to show when data is valid", %{conn: conn} do
|
||||
conn = post(conn, ~p"/students", student: @create_attrs)
|
||||
|
||||
assert %{id: id} = redirected_params(conn)
|
||||
assert redirected_to(conn) == ~p"/students/#{id}"
|
||||
|
||||
conn = get(conn, ~p"/students/#{id}")
|
||||
assert html_response(conn, 200) =~ "Student #{id}"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn} do
|
||||
conn = post(conn, ~p"/students", student: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "New Student"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit student" do
|
||||
setup [:create_student]
|
||||
|
||||
test "renders form for editing chosen student", %{conn: conn, student: student} do
|
||||
conn = get(conn, ~p"/students/#{student}/edit")
|
||||
assert html_response(conn, 200) =~ "Edit Student"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update student" do
|
||||
setup [:create_student]
|
||||
|
||||
test "redirects when data is valid", %{conn: conn, student: student} do
|
||||
conn = put(conn, ~p"/students/#{student}", student: @update_attrs)
|
||||
assert redirected_to(conn) == ~p"/students/#{student}"
|
||||
|
||||
conn = get(conn, ~p"/students/#{student}")
|
||||
assert html_response(conn, 200) =~ "A00000002"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, student: student} do
|
||||
conn = put(conn, ~p"/students/#{student}", student: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "Edit Student"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete student" do
|
||||
setup [:create_student]
|
||||
|
||||
test "deletes chosen student", %{conn: conn, student: student} do
|
||||
conn = delete(conn, ~p"/students/#{student}")
|
||||
assert redirected_to(conn) == ~p"/students"
|
||||
|
||||
assert_error_sent 404, fn ->
|
||||
get(conn, ~p"/students/#{student}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_student(_) do
|
||||
student = student_fixture()
|
||||
|
||||
%{student: student}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,147 @@
|
||||
defmodule BcitWeb.UserSessionControllerTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
import Bcit.AccountsFixtures
|
||||
alias Bcit.Accounts
|
||||
|
||||
setup do
|
||||
%{unconfirmed_user: unconfirmed_user_fixture(), user: user_fixture()}
|
||||
end
|
||||
|
||||
describe "POST /users/log-in - email and password" do
|
||||
test "logs the user in", %{conn: conn, user: user} do
|
||||
user = set_password(user)
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in", %{
|
||||
"user" => %{"email" => user.email, "password" => valid_user_password()}
|
||||
})
|
||||
|
||||
assert get_session(conn, :user_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
|
||||
# Now do a logged in request and assert on the menu
|
||||
conn = get(conn, ~p"/")
|
||||
response = html_response(conn, 200)
|
||||
assert response =~ user.email
|
||||
assert response =~ ~p"/users/settings"
|
||||
assert response =~ ~p"/users/log-out"
|
||||
end
|
||||
|
||||
test "logs the user in with remember me", %{conn: conn, user: user} do
|
||||
user = set_password(user)
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in", %{
|
||||
"user" => %{
|
||||
"email" => user.email,
|
||||
"password" => valid_user_password(),
|
||||
"remember_me" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert conn.resp_cookies["_bcit_web_user_remember_me"]
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
|
||||
test "logs the user in with return to", %{conn: conn, user: user} do
|
||||
user = set_password(user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> init_test_session(user_return_to: "/foo/bar")
|
||||
|> post(~p"/users/log-in", %{
|
||||
"user" => %{
|
||||
"email" => user.email,
|
||||
"password" => valid_user_password()
|
||||
}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/foo/bar"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
|
||||
end
|
||||
|
||||
test "redirects to login page with invalid credentials", %{conn: conn, user: user} do
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in?mode=password", %{
|
||||
"user" => %{"email" => user.email, "password" => "invalid_password"}
|
||||
})
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /users/log-in - magic link" do
|
||||
test "logs the user in", %{conn: conn, user: user} do
|
||||
{token, _hashed_token} = generate_user_magic_link_token(user)
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in", %{
|
||||
"user" => %{"token" => token}
|
||||
})
|
||||
|
||||
assert get_session(conn, :user_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
|
||||
# Now do a logged in request and assert on the menu
|
||||
conn = get(conn, ~p"/")
|
||||
response = html_response(conn, 200)
|
||||
assert response =~ user.email
|
||||
assert response =~ ~p"/users/settings"
|
||||
assert response =~ ~p"/users/log-out"
|
||||
end
|
||||
|
||||
test "confirms unconfirmed user", %{conn: conn, unconfirmed_user: user} do
|
||||
{token, _hashed_token} = generate_user_magic_link_token(user)
|
||||
refute user.confirmed_at
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in", %{
|
||||
"user" => %{"token" => token},
|
||||
"_action" => "confirmed"
|
||||
})
|
||||
|
||||
assert get_session(conn, :user_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "User confirmed successfully."
|
||||
|
||||
assert Accounts.get_user!(user.id).confirmed_at
|
||||
|
||||
# Now do a logged in request and assert on the menu
|
||||
conn = get(conn, ~p"/")
|
||||
response = html_response(conn, 200)
|
||||
assert response =~ user.email
|
||||
assert response =~ ~p"/users/settings"
|
||||
assert response =~ ~p"/users/log-out"
|
||||
end
|
||||
|
||||
test "redirects to login page when magic link is invalid", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, ~p"/users/log-in", %{
|
||||
"user" => %{"token" => "invalid"}
|
||||
})
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||
"The link is invalid or it has expired."
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /users/log-out" do
|
||||
test "logs the user out", %{conn: conn, user: user} do
|
||||
conn = conn |> log_in_user(user) |> delete(~p"/users/log-out")
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
refute get_session(conn, :user_token)
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
|
||||
end
|
||||
|
||||
test "succeeds even if the user is not logged in", %{conn: conn} do
|
||||
conn = delete(conn, ~p"/users/log-out")
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
refute get_session(conn, :user_token)
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
defmodule BcitWeb.UserLive.ConfirmationTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Bcit.AccountsFixtures
|
||||
|
||||
alias Bcit.Accounts
|
||||
|
||||
setup do
|
||||
%{unconfirmed_user: unconfirmed_user_fixture(), confirmed_user: user_fixture()}
|
||||
end
|
||||
|
||||
describe "Confirm user" do
|
||||
test "renders confirmation page for unconfirmed user", %{conn: conn, unconfirmed_user: user} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
|
||||
assert html =~ "Confirm and stay logged in"
|
||||
end
|
||||
|
||||
test "renders login page for confirmed user", %{conn: conn, confirmed_user: user} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
|
||||
refute html =~ "Confirm my account"
|
||||
assert html =~ "Keep me logged in on this device"
|
||||
end
|
||||
|
||||
test "renders login page for already logged in user", %{conn: conn, confirmed_user: user} do
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
|
||||
refute html =~ "Confirm my account"
|
||||
assert html =~ "Log in"
|
||||
end
|
||||
|
||||
test "confirms the given token once", %{conn: conn, unconfirmed_user: user} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
|
||||
|
||||
form = form(lv, "#confirmation_form", %{"user" => %{"token" => token}})
|
||||
render_submit(form)
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||
"User confirmed successfully"
|
||||
|
||||
assert Accounts.get_user!(user.id).confirmed_at
|
||||
# we are logged in now
|
||||
assert get_session(conn, :user_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
|
||||
# log out, new conn
|
||||
conn = build_conn()
|
||||
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/users/log-in/#{token}")
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
|
||||
test "logs confirmed user in without changing confirmed_at", %{
|
||||
conn: conn,
|
||||
confirmed_user: user
|
||||
} do
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
|
||||
|
||||
form = form(lv, "#login_form", %{"user" => %{"token" => token}})
|
||||
render_submit(form)
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||
"Welcome back!"
|
||||
|
||||
assert Accounts.get_user!(user.id).confirmed_at == user.confirmed_at
|
||||
|
||||
# log out, new conn
|
||||
conn = build_conn()
|
||||
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/users/log-in/#{token}")
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
|
||||
test "raises error for invalid token", %{conn: conn} do
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/users/log-in/invalid-token")
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,109 @@
|
||||
defmodule BcitWeb.UserLive.LoginTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Bcit.AccountsFixtures
|
||||
|
||||
describe "login page" do
|
||||
test "renders login page", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "Log in"
|
||||
assert html =~ "Sign up"
|
||||
assert html =~ "Log in with email"
|
||||
end
|
||||
end
|
||||
|
||||
describe "user login - magic link" do
|
||||
test "sends magic link email when user exists", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
{:ok, _lv, html} =
|
||||
form(lv, "#login_form_magic", user: %{email: user.email})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "If your email is in our system"
|
||||
|
||||
assert Bcit.Repo.get_by!(Bcit.Accounts.UserToken, user_id: user.id).context ==
|
||||
"login"
|
||||
end
|
||||
|
||||
test "does not disclose if user is registered", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
{:ok, _lv, html} =
|
||||
form(lv, "#login_form_magic", user: %{email: "idonotexist@example.com"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "If your email is in our system"
|
||||
end
|
||||
end
|
||||
|
||||
describe "user login - password" do
|
||||
test "redirects if user logs in with valid credentials", %{conn: conn} do
|
||||
user = user_fixture() |> set_password()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
form =
|
||||
form(lv, "#login_form_password",
|
||||
user: %{email: user.email, password: valid_user_password(), remember_me: true}
|
||||
)
|
||||
|
||||
conn = submit_form(form, conn)
|
||||
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
|
||||
test "redirects to login page with a flash error if credentials are invalid", %{
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
form =
|
||||
form(lv, "#login_form_password", user: %{email: "test@email.com", password: "123456"})
|
||||
|
||||
render_submit(form, %{user: %{remember_me: true}})
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "login navigation" do
|
||||
test "redirects to registration page when the Register button is clicked", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
{:ok, _login_live, login_html} =
|
||||
lv
|
||||
|> element("main a", "Sign up")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/users/register")
|
||||
|
||||
assert login_html =~ "Register"
|
||||
end
|
||||
end
|
||||
|
||||
describe "re-authentication (sudo mode)" do
|
||||
setup %{conn: conn} do
|
||||
user = user_fixture()
|
||||
%{user: user, conn: log_in_user(conn, user)}
|
||||
end
|
||||
|
||||
test "shows login page with email filled in", %{conn: conn, user: user} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~ "You need to reauthenticate"
|
||||
refute html =~ "Register"
|
||||
assert html =~ "Log in with email"
|
||||
|
||||
assert html =~
|
||||
~s(<input type="email" name="user[email]" id="login_form_magic_email" value="#{user.email}")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
defmodule BcitWeb.UserLive.RegistrationTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Bcit.AccountsFixtures
|
||||
|
||||
describe "Registration page" do
|
||||
test "renders registration page", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/users/register")
|
||||
|
||||
assert html =~ "Register"
|
||||
assert html =~ "Log in"
|
||||
end
|
||||
|
||||
test "redirects if already logged in", %{conn: conn} do
|
||||
result =
|
||||
conn
|
||||
|> log_in_user(user_fixture())
|
||||
|> live(~p"/users/register")
|
||||
|> follow_redirect(conn, ~p"/")
|
||||
|
||||
assert {:ok, _conn} = result
|
||||
end
|
||||
|
||||
test "renders errors for invalid data", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#registration_form")
|
||||
|> render_change(user: %{"email" => "with spaces"})
|
||||
|
||||
assert result =~ "Register"
|
||||
assert result =~ "must have the @ sign and no spaces"
|
||||
end
|
||||
end
|
||||
|
||||
describe "register user" do
|
||||
test "creates account but does not log in", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||
|
||||
email = unique_user_email()
|
||||
form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
|
||||
|
||||
{:ok, _lv, html} =
|
||||
render_submit(form)
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert html =~
|
||||
~r/An email was sent to .*, please access it to confirm your account/
|
||||
end
|
||||
|
||||
test "renders errors for duplicated email", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||
|
||||
user = user_fixture(%{email: "test@email.com"})
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#registration_form",
|
||||
user: %{"email" => user.email}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "has already been taken"
|
||||
end
|
||||
end
|
||||
|
||||
describe "registration navigation" do
|
||||
test "redirects to login page when the Log in button is clicked", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||
|
||||
{:ok, _login_live, login_html} =
|
||||
lv
|
||||
|> element("main a", "Log in")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert login_html =~ "Log in"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,212 @@
|
||||
defmodule BcitWeb.UserLive.SettingsTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
alias Bcit.Accounts
|
||||
import Phoenix.LiveViewTest
|
||||
import Bcit.AccountsFixtures
|
||||
|
||||
describe "Settings page" do
|
||||
test "renders settings page", %{conn: conn} do
|
||||
{:ok, _lv, html} =
|
||||
conn
|
||||
|> log_in_user(user_fixture())
|
||||
|> live(~p"/users/settings")
|
||||
|
||||
assert html =~ "Change Email"
|
||||
assert html =~ "Save Password"
|
||||
end
|
||||
|
||||
test "redirects if user is not logged in", %{conn: conn} do
|
||||
assert {:error, redirect} = live(conn, ~p"/users/settings")
|
||||
|
||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
assert %{"error" => "You must log in to access this page."} = flash
|
||||
end
|
||||
|
||||
test "redirects if user is not in sudo mode", %{conn: conn} do
|
||||
{:ok, conn} =
|
||||
conn
|
||||
|> log_in_user(user_fixture(),
|
||||
token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute)
|
||||
)
|
||||
|> live(~p"/users/settings")
|
||||
|> follow_redirect(conn, ~p"/users/log-in")
|
||||
|
||||
assert conn.resp_body =~ "You must re-authenticate to access this page."
|
||||
end
|
||||
end
|
||||
|
||||
describe "update email form" do
|
||||
setup %{conn: conn} do
|
||||
user = user_fixture()
|
||||
%{conn: log_in_user(conn, user), user: user}
|
||||
end
|
||||
|
||||
test "updates the user email", %{conn: conn, user: user} do
|
||||
new_email = unique_user_email()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#email_form", %{
|
||||
"user" => %{"email" => new_email}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "A link to confirm your email"
|
||||
assert Accounts.get_user_by_email(user.email)
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#email_form")
|
||||
|> render_change(%{
|
||||
"action" => "update_email",
|
||||
"user" => %{"email" => "with spaces"}
|
||||
})
|
||||
|
||||
assert result =~ "Change Email"
|
||||
assert result =~ "must have the @ sign and no spaces"
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#email_form", %{
|
||||
"user" => %{"email" => user.email}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "Change Email"
|
||||
assert result =~ "did not change"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update password form" do
|
||||
setup %{conn: conn} do
|
||||
user = user_fixture()
|
||||
%{conn: log_in_user(conn, user), user: user}
|
||||
end
|
||||
|
||||
test "updates the user password", %{conn: conn, user: user} do
|
||||
new_password = valid_user_password()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
form =
|
||||
form(lv, "#password_form", %{
|
||||
"user" => %{
|
||||
"email" => user.email,
|
||||
"password" => new_password,
|
||||
"password_confirmation" => new_password
|
||||
}
|
||||
})
|
||||
|
||||
render_submit(form)
|
||||
|
||||
new_password_conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert redirected_to(new_password_conn) == ~p"/users/settings"
|
||||
|
||||
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
|
||||
|
||||
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
||||
"Password updated successfully"
|
||||
|
||||
assert Accounts.get_user_by_email_and_password(user.email, new_password)
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#password_form")
|
||||
|> render_change(%{
|
||||
"user" => %{
|
||||
"password" => "too short",
|
||||
"password_confirmation" => "does not match"
|
||||
}
|
||||
})
|
||||
|
||||
assert result =~ "Save Password"
|
||||
assert result =~ "should be at least 12 character(s)"
|
||||
assert result =~ "does not match password"
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-submit)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#password_form", %{
|
||||
"user" => %{
|
||||
"password" => "too short",
|
||||
"password_confirmation" => "does not match"
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "Save Password"
|
||||
assert result =~ "should be at least 12 character(s)"
|
||||
assert result =~ "does not match password"
|
||||
end
|
||||
end
|
||||
|
||||
describe "confirm email" do
|
||||
setup %{conn: conn} do
|
||||
user = user_fixture()
|
||||
email = unique_user_email()
|
||||
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
||||
end)
|
||||
|
||||
%{conn: log_in_user(conn, user), token: token, email: email, user: user}
|
||||
end
|
||||
|
||||
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/users/settings"
|
||||
assert %{"info" => message} = flash
|
||||
assert message == "Email changed successfully."
|
||||
refute Accounts.get_user_by_email(user.email)
|
||||
assert Accounts.get_user_by_email(email)
|
||||
|
||||
# use confirm token again
|
||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/users/settings"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "Email change link is invalid or it has expired."
|
||||
end
|
||||
|
||||
test "does not update email with invalid token", %{conn: conn, user: user} do
|
||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops")
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/users/settings"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "Email change link is invalid or it has expired."
|
||||
assert Accounts.get_user_by_email(user.email)
|
||||
end
|
||||
|
||||
test "redirects if user is not logged in", %{token: token} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "You must log in to access this page."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,401 @@
|
||||
defmodule BcitWeb.UserAuthTest do
|
||||
use BcitWeb.ConnCase, async: true
|
||||
|
||||
alias Phoenix.LiveView
|
||||
alias Bcit.Accounts
|
||||
alias Bcit.Accounts.Scope
|
||||
alias BcitWeb.UserAuth
|
||||
|
||||
import Bcit.AccountsFixtures
|
||||
|
||||
@remember_me_cookie "_bcit_web_user_remember_me"
|
||||
@remember_me_cookie_max_age 60 * 60 * 24 * 14
|
||||
|
||||
setup %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> Map.replace!(:secret_key_base, BcitWeb.Endpoint.config(:secret_key_base))
|
||||
|> init_test_session(%{})
|
||||
|
||||
%{user: %{user_fixture() | authenticated_at: DateTime.utc_now(:second)}, conn: conn}
|
||||
end
|
||||
|
||||
describe "log_in_user/3" do
|
||||
test "stores the user token in the session", %{conn: conn, user: user} do
|
||||
conn = UserAuth.log_in_user(conn, user)
|
||||
assert token = get_session(conn, :user_token)
|
||||
assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
assert Accounts.get_user_by_session_token(token)
|
||||
end
|
||||
|
||||
test "clears everything previously stored in the session", %{conn: conn, user: user} do
|
||||
conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
|
||||
refute get_session(conn, :to_be_removed)
|
||||
end
|
||||
|
||||
test "keeps session when re-authenticating", %{conn: conn, user: user} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> put_session(:to_be_removed, "value")
|
||||
|> UserAuth.log_in_user(user)
|
||||
|
||||
assert get_session(conn, :to_be_removed)
|
||||
end
|
||||
|
||||
test "clears session when user does not match when re-authenticating", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
other_user = user_fixture()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:current_scope, Scope.for_user(other_user))
|
||||
|> put_session(:to_be_removed, "value")
|
||||
|> UserAuth.log_in_user(user)
|
||||
|
||||
refute get_session(conn, :to_be_removed)
|
||||
end
|
||||
|
||||
test "redirects to the configured path", %{conn: conn, user: user} do
|
||||
conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
|
||||
assert redirected_to(conn) == "/hello"
|
||||
end
|
||||
|
||||
test "clears the return-to path from the session after logging in", %{conn: conn, user: user} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> put_session(:user_return_to, "/hello")
|
||||
|> UserAuth.log_in_user(user)
|
||||
|
||||
assert redirected_to(conn) == "/hello"
|
||||
refute get_session(conn, :user_return_to)
|
||||
end
|
||||
|
||||
test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
|
||||
conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
|
||||
assert get_session(conn, :user_remember_me) == true
|
||||
|
||||
assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert signed_token != get_session(conn, :user_token)
|
||||
assert max_age == @remember_me_cookie_max_age
|
||||
end
|
||||
|
||||
test "redirects to settings when user is already logged in", %{conn: conn, user: user} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.log_in_user(user)
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/settings"
|
||||
end
|
||||
|
||||
test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do
|
||||
conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
|
||||
assert get_session(conn, :user_remember_me) == true
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> recycle()
|
||||
|> Map.replace!(:secret_key_base, BcitWeb.Endpoint.config(:secret_key_base))
|
||||
|> fetch_cookies()
|
||||
|> init_test_session(%{user_remember_me: true})
|
||||
|
||||
# the conn is already logged in and has the remember_me cookie set,
|
||||
# now we log in again and even without explicitly setting remember_me,
|
||||
# the cookie should be set again
|
||||
conn = conn |> UserAuth.log_in_user(user, %{})
|
||||
assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert signed_token != get_session(conn, :user_token)
|
||||
assert max_age == @remember_me_cookie_max_age
|
||||
assert get_session(conn, :user_remember_me) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "logout_user/1" do
|
||||
test "erases session and cookies", %{conn: conn, user: user} do
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_token, user_token)
|
||||
|> put_req_cookie(@remember_me_cookie, user_token)
|
||||
|> fetch_cookies()
|
||||
|> UserAuth.log_out_user()
|
||||
|
||||
refute get_session(conn, :user_token)
|
||||
refute conn.cookies[@remember_me_cookie]
|
||||
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
refute Accounts.get_user_by_session_token(user_token)
|
||||
end
|
||||
|
||||
test "broadcasts to the given live_socket_id", %{conn: conn} do
|
||||
live_socket_id = "users_sessions:abcdef-token"
|
||||
BcitWeb.Endpoint.subscribe(live_socket_id)
|
||||
|
||||
conn
|
||||
|> put_session(:live_socket_id, live_socket_id)
|
||||
|> UserAuth.log_out_user()
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
|
||||
end
|
||||
|
||||
test "works even if user is already logged out", %{conn: conn} do
|
||||
conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
|
||||
refute get_session(conn, :user_token)
|
||||
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_current_scope_for_user/2" do
|
||||
test "authenticates user from session", %{conn: conn, user: user} do
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
|
||||
conn =
|
||||
conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_scope_for_user([])
|
||||
|
||||
assert conn.assigns.current_scope.user.id == user.id
|
||||
assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
|
||||
assert get_session(conn, :user_token) == user_token
|
||||
end
|
||||
|
||||
test "authenticates user from cookies", %{conn: conn, user: user} do
|
||||
logged_in_conn =
|
||||
conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
|
||||
user_token = logged_in_conn.cookies[@remember_me_cookie]
|
||||
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_cookie(@remember_me_cookie, signed_token)
|
||||
|> UserAuth.fetch_current_scope_for_user([])
|
||||
|
||||
assert conn.assigns.current_scope.user.id == user.id
|
||||
assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
|
||||
assert get_session(conn, :user_token) == user_token
|
||||
assert get_session(conn, :user_remember_me)
|
||||
|
||||
assert get_session(conn, :live_socket_id) ==
|
||||
"users_sessions:#{Base.url_encode64(user_token)}"
|
||||
end
|
||||
|
||||
test "does not authenticate if data is missing", %{conn: conn, user: user} do
|
||||
_ = Accounts.generate_user_session_token(user)
|
||||
conn = UserAuth.fetch_current_scope_for_user(conn, [])
|
||||
refute get_session(conn, :user_token)
|
||||
refute conn.assigns.current_scope
|
||||
end
|
||||
|
||||
test "reissues a new token after a few days and refreshes cookie", %{conn: conn, user: user} do
|
||||
logged_in_conn =
|
||||
conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
|
||||
token = logged_in_conn.cookies[@remember_me_cookie]
|
||||
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
|
||||
|
||||
offset_user_token(token, -10, :day)
|
||||
{user, _} = Accounts.get_user_by_session_token(token)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_token, token)
|
||||
|> put_session(:user_remember_me, true)
|
||||
|> put_req_cookie(@remember_me_cookie, signed_token)
|
||||
|> UserAuth.fetch_current_scope_for_user([])
|
||||
|
||||
assert conn.assigns.current_scope.user.id == user.id
|
||||
assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
|
||||
assert new_token = get_session(conn, :user_token)
|
||||
assert new_token != token
|
||||
assert %{value: new_signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert new_signed_token != signed_token
|
||||
assert max_age == @remember_me_cookie_max_age
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount :mount_current_scope" do
|
||||
setup %{conn: conn} do
|
||||
%{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
|
||||
end
|
||||
|
||||
test "assigns current_scope based on a valid user_token", %{conn: conn, user: user} do
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
{:cont, updated_socket} =
|
||||
UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
|
||||
|
||||
assert updated_socket.assigns.current_scope.user.id == user.id
|
||||
end
|
||||
|
||||
test "assigns nil to current_scope assign if there isn't a valid user_token", %{conn: conn} do
|
||||
user_token = "invalid_token"
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
{:cont, updated_socket} =
|
||||
UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
|
||||
|
||||
assert updated_socket.assigns.current_scope == nil
|
||||
end
|
||||
|
||||
test "assigns nil to current_scope assign if there isn't a user_token", %{conn: conn} do
|
||||
session = conn |> get_session()
|
||||
|
||||
{:cont, updated_socket} =
|
||||
UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
|
||||
|
||||
assert updated_socket.assigns.current_scope == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount :require_authenticated" do
|
||||
test "authenticates current_scope based on a valid user_token", %{conn: conn, user: user} do
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
{:cont, updated_socket} =
|
||||
UserAuth.on_mount(:require_authenticated, %{}, session, %LiveView.Socket{})
|
||||
|
||||
assert updated_socket.assigns.current_scope.user.id == user.id
|
||||
end
|
||||
|
||||
test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
|
||||
user_token = "invalid_token"
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: BcitWeb.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}}
|
||||
}
|
||||
|
||||
{:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
|
||||
assert updated_socket.assigns.current_scope == nil
|
||||
end
|
||||
|
||||
test "redirects to login page if there isn't a user_token", %{conn: conn} do
|
||||
session = conn |> get_session()
|
||||
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: BcitWeb.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}}
|
||||
}
|
||||
|
||||
{:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
|
||||
assert updated_socket.assigns.current_scope == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount :require_sudo_mode" do
|
||||
test "allows users that have authenticated in the last 10 minutes", %{conn: conn, user: user} do
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: BcitWeb.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}}
|
||||
}
|
||||
|
||||
assert {:cont, _updated_socket} =
|
||||
UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
|
||||
end
|
||||
|
||||
test "redirects when authentication is too old", %{conn: conn, user: user} do
|
||||
eleven_minutes_ago = DateTime.utc_now(:second) |> DateTime.add(-11, :minute)
|
||||
user = %{user | authenticated_at: eleven_minutes_ago}
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
{user, token_inserted_at} = Accounts.get_user_by_session_token(user_token)
|
||||
assert DateTime.compare(token_inserted_at, user.authenticated_at) == :gt
|
||||
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: BcitWeb.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}}
|
||||
}
|
||||
|
||||
assert {:halt, _updated_socket} =
|
||||
UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
|
||||
end
|
||||
end
|
||||
|
||||
describe "require_authenticated_user/2" do
|
||||
setup %{conn: conn} do
|
||||
%{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
|
||||
end
|
||||
|
||||
test "redirects if user is not authenticated", %{conn: conn} do
|
||||
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
|
||||
assert conn.halted
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||
"You must log in to access this page."
|
||||
end
|
||||
|
||||
test "stores the path to redirect to on GET", %{conn: conn} do
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: ""}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar=baz"}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
refute get_session(halted_conn, :user_return_to)
|
||||
end
|
||||
|
||||
test "does not redirect if user is authenticated", %{conn: conn, user: user} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
end
|
||||
|
||||
describe "disconnect_sessions/1" do
|
||||
test "broadcasts disconnect messages for each token" do
|
||||
tokens = [%{token: "token1"}, %{token: "token2"}]
|
||||
|
||||
for %{token: token} <- tokens do
|
||||
BcitWeb.Endpoint.subscribe("users_sessions:#{Base.url_encode64(token)}")
|
||||
end
|
||||
|
||||
UserAuth.disconnect_sessions(tokens)
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
event: "disconnect",
|
||||
topic: "users_sessions:dG9rZW4x"
|
||||
}
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
event: "disconnect",
|
||||
topic: "users_sessions:dG9rZW4y"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
defmodule BcitWeb.ConnCase do
|
||||
@moduledoc """
|
||||
This module defines the test case to be used by
|
||||
tests that require setting up a connection.
|
||||
|
||||
Such tests rely on `Phoenix.ConnTest` and also
|
||||
import other functionality to make it easier
|
||||
to build common data structures and query the data layer.
|
||||
|
||||
Finally, if the test case interacts with the database,
|
||||
we enable the SQL sandbox, so changes done to the database
|
||||
are reverted at the end of every test. If you are using
|
||||
PostgreSQL, you can even run database tests asynchronously
|
||||
by setting `use BcitWeb.ConnCase, async: true`, although
|
||||
this option is not recommended for other databases.
|
||||
"""
|
||||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using do
|
||||
quote do
|
||||
# The default endpoint for testing
|
||||
@endpoint BcitWeb.Endpoint
|
||||
|
||||
use BcitWeb, :verified_routes
|
||||
|
||||
# Import conveniences for testing with connections
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
import BcitWeb.ConnCase
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
Bcit.DataCase.setup_sandbox(tags)
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Setup helper that registers and logs in users.
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
It stores an updated connection and a registered user in the
|
||||
test context.
|
||||
"""
|
||||
def register_and_log_in_user(%{conn: conn} = context) do
|
||||
user = Bcit.AccountsFixtures.user_fixture()
|
||||
scope = Bcit.Accounts.Scope.for_user(user)
|
||||
|
||||
opts =
|
||||
context
|
||||
|> Map.take([:token_authenticated_at])
|
||||
|> Enum.into([])
|
||||
|
||||
%{conn: log_in_user(conn, user, opts), user: user, scope: scope}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs the given `user` into the `conn`.
|
||||
|
||||
It returns an updated `conn`.
|
||||
"""
|
||||
def log_in_user(conn, user, opts \\ []) do
|
||||
token = Bcit.Accounts.generate_user_session_token(user)
|
||||
|
||||
maybe_set_token_authenticated_at(token, opts[:token_authenticated_at])
|
||||
|
||||
conn
|
||||
|> Phoenix.ConnTest.init_test_session(%{})
|
||||
|> Plug.Conn.put_session(:user_token, token)
|
||||
end
|
||||
|
||||
defp maybe_set_token_authenticated_at(_token, nil), do: nil
|
||||
|
||||
defp maybe_set_token_authenticated_at(token, authenticated_at) do
|
||||
Bcit.AccountsFixtures.override_token_authenticated_at(token, authenticated_at)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
defmodule Bcit.DataCase do
|
||||
@moduledoc """
|
||||
This module defines the setup for tests requiring
|
||||
access to the application's data layer.
|
||||
|
||||
You may define functions here to be used as helpers in
|
||||
your tests.
|
||||
|
||||
Finally, if the test case interacts with the database,
|
||||
we enable the SQL sandbox, so changes done to the database
|
||||
are reverted at the end of every test. If you are using
|
||||
PostgreSQL, you can even run database tests asynchronously
|
||||
by setting `use Bcit.DataCase, async: true`, although
|
||||
this option is not recommended for other databases.
|
||||
"""
|
||||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using do
|
||||
quote do
|
||||
alias Bcit.Repo
|
||||
|
||||
import Ecto
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Bcit.DataCase
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
Bcit.DataCase.setup_sandbox(tags)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets up the sandbox based on the test tags.
|
||||
"""
|
||||
def setup_sandbox(tags) do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Bcit.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
A helper that transforms changeset errors into a map of messages.
|
||||
|
||||
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
|
||||
assert "password is too short" in errors_on(changeset).password
|
||||
assert %{password: ["password is too short"]} = errors_on(changeset)
|
||||
|
||||
"""
|
||||
def errors_on(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
defmodule Bcit.AccountsFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Bcit.Accounts` context.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Bcit.Accounts
|
||||
alias Bcit.Accounts.Scope
|
||||
|
||||
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||
def valid_user_password, do: "hello world!"
|
||||
|
||||
def valid_user_attributes(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
email: unique_user_email()
|
||||
})
|
||||
end
|
||||
|
||||
def unconfirmed_user_fixture(attrs \\ %{}) do
|
||||
{:ok, user} =
|
||||
attrs
|
||||
|> valid_user_attributes()
|
||||
|> Accounts.register_user()
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def user_fixture(attrs \\ %{}) do
|
||||
user = unconfirmed_user_fixture(attrs)
|
||||
|
||||
token =
|
||||
extract_user_token(fn url ->
|
||||
Accounts.deliver_login_instructions(user, url)
|
||||
end)
|
||||
|
||||
{:ok, {user, _expired_tokens}} =
|
||||
Accounts.login_user_by_magic_link(token)
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def user_scope_fixture do
|
||||
user = user_fixture()
|
||||
user_scope_fixture(user)
|
||||
end
|
||||
|
||||
def user_scope_fixture(user) do
|
||||
Scope.for_user(user)
|
||||
end
|
||||
|
||||
def set_password(user) do
|
||||
{:ok, {user, _expired_tokens}} =
|
||||
Accounts.update_user_password(user, %{password: valid_user_password()})
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def extract_user_token(fun) do
|
||||
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
||||
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
|
||||
token
|
||||
end
|
||||
|
||||
def override_token_authenticated_at(token, authenticated_at) when is_binary(token) do
|
||||
Bcit.Repo.update_all(
|
||||
from(t in Accounts.UserToken,
|
||||
where: t.token == ^token
|
||||
),
|
||||
set: [authenticated_at: authenticated_at]
|
||||
)
|
||||
end
|
||||
|
||||
def generate_user_magic_link_token(user) do
|
||||
{encoded_token, user_token} = Accounts.UserToken.build_email_token(user, "login")
|
||||
Bcit.Repo.insert!(user_token)
|
||||
{encoded_token, user_token.token}
|
||||
end
|
||||
|
||||
def offset_user_token(token, amount_to_add, unit) do
|
||||
dt = DateTime.add(DateTime.utc_now(:second), amount_to_add, unit)
|
||||
|
||||
Bcit.Repo.update_all(
|
||||
from(ut in Accounts.UserToken, where: ut.token == ^token),
|
||||
set: [inserted_at: dt, authenticated_at: dt]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
defmodule Bcit.SchoolsFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Bcit.Schools` context.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generate a unique student sid.
|
||||
"""
|
||||
def unique_student_sid do
|
||||
"A" <> (System.unique_integer([:positive]) |> Integer.to_string() |> String.pad_leading(8, "0"))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate a student.
|
||||
"""
|
||||
def student_fixture(attrs \\ %{}) do
|
||||
{:ok, student} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
firstname: "some firstname",
|
||||
gpa: 85.5,
|
||||
lastname: "some lastname",
|
||||
sid: unique_student_sid()
|
||||
})
|
||||
|> Bcit.Schools.create_student()
|
||||
|
||||
student
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Bcit.Repo, :manual)
|
||||
Reference in new issue
Block a user