daily-tracker

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

accounts_test.exs (13315B)


      1 defmodule DailyTracker.AccountsTest do
      2   use DailyTracker.DataCase
      3 
      4   alias DailyTracker.Accounts
      5 
      6   import DailyTracker.AccountsFixtures
      7   alias DailyTracker.Accounts.{User, UserToken}
      8 
      9   describe "get_user_by_email/1" do
     10     test "does not return the user if the email does not exist" do
     11       refute Accounts.get_user_by_email("unknown@example.com")
     12     end
     13 
     14     test "returns the user if the email exists" do
     15       %{id: id} = user = user_fixture()
     16       assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
     17     end
     18   end
     19 
     20   describe "get_user_by_email_and_password/2" do
     21     test "does not return the user if the email does not exist" do
     22       refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
     23     end
     24 
     25     test "does not return the user if the password is not valid" do
     26       user = user_fixture() |> set_password()
     27       refute Accounts.get_user_by_email_and_password(user.email, "invalid")
     28     end
     29 
     30     test "returns the user if the email and password are valid" do
     31       %{id: id} = user = user_fixture() |> set_password()
     32 
     33       assert %User{id: ^id} =
     34                Accounts.get_user_by_email_and_password(user.email, valid_user_password())
     35     end
     36   end
     37 
     38   describe "get_user!/1" do
     39     test "raises if id is invalid" do
     40       assert_raise Ecto.NoResultsError, fn ->
     41         Accounts.get_user!(-1)
     42       end
     43     end
     44 
     45     test "returns the user with the given id" do
     46       %{id: id} = user = user_fixture()
     47       assert %User{id: ^id} = Accounts.get_user!(user.id)
     48     end
     49   end
     50 
     51   describe "register_user/1" do
     52     test "requires email to be set" do
     53       {:error, changeset} = Accounts.register_user(%{})
     54 
     55       assert %{email: ["can't be blank"]} = errors_on(changeset)
     56     end
     57 
     58     test "validates email when given" do
     59       {:error, changeset} = Accounts.register_user(%{email: "not valid"})
     60 
     61       assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
     62     end
     63 
     64     test "validates maximum values for email for security" do
     65       too_long = String.duplicate("db", 100)
     66       {:error, changeset} = Accounts.register_user(%{email: too_long})
     67       assert "should be at most 160 character(s)" in errors_on(changeset).email
     68     end
     69 
     70     test "validates email uniqueness" do
     71       %{email: email} = user_fixture()
     72       {:error, changeset} = Accounts.register_user(%{email: email})
     73       assert "has already been taken" in errors_on(changeset).email
     74 
     75       # Now try with the uppercased email too, to check that email case is ignored.
     76       {:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
     77       assert "has already been taken" in errors_on(changeset).email
     78     end
     79 
     80     test "registers users without password" do
     81       email = unique_user_email()
     82       {:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
     83       assert user.email == email
     84       assert is_nil(user.hashed_password)
     85       assert is_nil(user.confirmed_at)
     86       assert is_nil(user.password)
     87     end
     88   end
     89 
     90   describe "sudo_mode?/2" do
     91     test "validates the authenticated_at time" do
     92       now = DateTime.utc_now()
     93 
     94       assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()})
     95       assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)})
     96       refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)})
     97 
     98       # minute override
     99       refute Accounts.sudo_mode?(
    100                %User{authenticated_at: DateTime.add(now, -11, :minute)},
    101                -10
    102              )
    103 
    104       # not authenticated
    105       refute Accounts.sudo_mode?(%User{})
    106     end
    107   end
    108 
    109   describe "change_user_email/3" do
    110     test "returns a user changeset" do
    111       assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
    112       assert changeset.required == [:email]
    113     end
    114   end
    115 
    116   describe "deliver_user_update_email_instructions/3" do
    117     setup do
    118       %{user: user_fixture()}
    119     end
    120 
    121     test "sends token through notification", %{user: user} do
    122       token =
    123         extract_user_token(fn url ->
    124           Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
    125         end)
    126 
    127       {:ok, token} = Base.url_decode64(token, padding: false)
    128       assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
    129       assert user_token.user_id == user.id
    130       assert user_token.sent_to == user.email
    131       assert user_token.context == "change:current@example.com"
    132     end
    133   end
    134 
    135   describe "update_user_email/2" do
    136     setup do
    137       user = unconfirmed_user_fixture()
    138       email = unique_user_email()
    139 
    140       token =
    141         extract_user_token(fn url ->
    142           Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
    143         end)
    144 
    145       %{user: user, token: token, email: email}
    146     end
    147 
    148     test "updates the email with a valid token", %{user: user, token: token, email: email} do
    149       assert {:ok, %{email: ^email}} = Accounts.update_user_email(user, token)
    150       changed_user = Repo.get!(User, user.id)
    151       assert changed_user.email != user.email
    152       assert changed_user.email == email
    153       refute Repo.get_by(UserToken, user_id: user.id)
    154     end
    155 
    156     test "does not update email with invalid token", %{user: user} do
    157       assert Accounts.update_user_email(user, "oops") ==
    158                {:error, :transaction_aborted}
    159 
    160       assert Repo.get!(User, user.id).email == user.email
    161       assert Repo.get_by(UserToken, user_id: user.id)
    162     end
    163 
    164     test "does not update email if user email changed", %{user: user, token: token} do
    165       assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) ==
    166                {:error, :transaction_aborted}
    167 
    168       assert Repo.get!(User, user.id).email == user.email
    169       assert Repo.get_by(UserToken, user_id: user.id)
    170     end
    171 
    172     test "does not update email if token expired", %{user: user, token: token} do
    173       {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
    174 
    175       assert Accounts.update_user_email(user, token) ==
    176                {:error, :transaction_aborted}
    177 
    178       assert Repo.get!(User, user.id).email == user.email
    179       assert Repo.get_by(UserToken, user_id: user.id)
    180     end
    181   end
    182 
    183   describe "change_user_password/3" do
    184     test "returns a user changeset" do
    185       assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
    186       assert changeset.required == [:password]
    187     end
    188 
    189     test "allows fields to be set" do
    190       changeset =
    191         Accounts.change_user_password(
    192           %User{},
    193           %{
    194             "password" => "new valid password"
    195           },
    196           hash_password: false
    197         )
    198 
    199       assert changeset.valid?
    200       assert get_change(changeset, :password) == "new valid password"
    201       assert is_nil(get_change(changeset, :hashed_password))
    202     end
    203   end
    204 
    205   describe "update_user_password/2" do
    206     setup do
    207       %{user: user_fixture()}
    208     end
    209 
    210     test "validates password", %{user: user} do
    211       {:error, changeset} =
    212         Accounts.update_user_password(user, %{
    213           password: "not valid",
    214           password_confirmation: "another"
    215         })
    216 
    217       assert %{
    218                password: ["should be at least 12 character(s)"],
    219                password_confirmation: ["does not match password"]
    220              } = errors_on(changeset)
    221     end
    222 
    223     test "validates maximum values for password for security", %{user: user} do
    224       too_long = String.duplicate("db", 100)
    225 
    226       {:error, changeset} =
    227         Accounts.update_user_password(user, %{password: too_long})
    228 
    229       assert "should be at most 72 character(s)" in errors_on(changeset).password
    230     end
    231 
    232     test "updates the password", %{user: user} do
    233       {:ok, {user, expired_tokens}} =
    234         Accounts.update_user_password(user, %{
    235           password: "new valid password"
    236         })
    237 
    238       assert expired_tokens == []
    239       assert is_nil(user.password)
    240       assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
    241     end
    242 
    243     test "deletes all tokens for the given user", %{user: user} do
    244       _ = Accounts.generate_user_session_token(user)
    245 
    246       {:ok, {_, _}} =
    247         Accounts.update_user_password(user, %{
    248           password: "new valid password"
    249         })
    250 
    251       refute Repo.get_by(UserToken, user_id: user.id)
    252     end
    253   end
    254 
    255   describe "generate_user_session_token/1" do
    256     setup do
    257       %{user: user_fixture()}
    258     end
    259 
    260     test "generates a token", %{user: user} do
    261       token = Accounts.generate_user_session_token(user)
    262       assert user_token = Repo.get_by(UserToken, token: token)
    263       assert user_token.context == "session"
    264       assert user_token.authenticated_at != nil
    265 
    266       # Creating the same token for another user should fail
    267       assert_raise Ecto.ConstraintError, fn ->
    268         Repo.insert!(%UserToken{
    269           token: user_token.token,
    270           user_id: user_fixture().id,
    271           context: "session"
    272         })
    273       end
    274     end
    275 
    276     test "duplicates the authenticated_at of given user in new token", %{user: user} do
    277       user = %{user | authenticated_at: DateTime.add(DateTime.utc_now(:second), -3600)}
    278       token = Accounts.generate_user_session_token(user)
    279       assert user_token = Repo.get_by(UserToken, token: token)
    280       assert user_token.authenticated_at == user.authenticated_at
    281       assert DateTime.compare(user_token.inserted_at, user.authenticated_at) == :gt
    282     end
    283   end
    284 
    285   describe "get_user_by_session_token/1" do
    286     setup do
    287       user = user_fixture()
    288       token = Accounts.generate_user_session_token(user)
    289       %{user: user, token: token}
    290     end
    291 
    292     test "returns user by token", %{user: user, token: token} do
    293       assert {session_user, token_inserted_at} = Accounts.get_user_by_session_token(token)
    294       assert session_user.id == user.id
    295       assert session_user.authenticated_at != nil
    296       assert token_inserted_at != nil
    297     end
    298 
    299     test "does not return user for invalid token" do
    300       refute Accounts.get_user_by_session_token("oops")
    301     end
    302 
    303     test "does not return user for expired token", %{token: token} do
    304       dt = ~N[2020-01-01 00:00:00]
    305       {1, nil} = Repo.update_all(UserToken, set: [inserted_at: dt, authenticated_at: dt])
    306       refute Accounts.get_user_by_session_token(token)
    307     end
    308   end
    309 
    310   describe "get_user_by_magic_link_token/1" do
    311     setup do
    312       user = user_fixture()
    313       {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
    314       %{user: user, token: encoded_token}
    315     end
    316 
    317     test "returns user by token", %{user: user, token: token} do
    318       assert session_user = Accounts.get_user_by_magic_link_token(token)
    319       assert session_user.id == user.id
    320     end
    321 
    322     test "does not return user for invalid token" do
    323       refute Accounts.get_user_by_magic_link_token("oops")
    324     end
    325 
    326     test "does not return user for expired token", %{token: token} do
    327       {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
    328       refute Accounts.get_user_by_magic_link_token(token)
    329     end
    330   end
    331 
    332   describe "login_user_by_magic_link/1" do
    333     test "confirms user and expires tokens" do
    334       user = unconfirmed_user_fixture()
    335       refute user.confirmed_at
    336       {encoded_token, hashed_token} = generate_user_magic_link_token(user)
    337 
    338       assert {:ok, {user, [%{token: ^hashed_token}]}} =
    339                Accounts.login_user_by_magic_link(encoded_token)
    340 
    341       assert user.confirmed_at
    342     end
    343 
    344     test "returns user and (deleted) token for confirmed user" do
    345       user = user_fixture()
    346       assert user.confirmed_at
    347       {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
    348       assert {:ok, {^user, []}} = Accounts.login_user_by_magic_link(encoded_token)
    349       # one time use only
    350       assert {:error, :not_found} = Accounts.login_user_by_magic_link(encoded_token)
    351     end
    352 
    353     test "raises when unconfirmed user has password set" do
    354       user = unconfirmed_user_fixture()
    355       {1, nil} = Repo.update_all(User, set: [hashed_password: "hashed"])
    356       {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
    357 
    358       assert_raise RuntimeError, ~r/magic link log in is not allowed/, fn ->
    359         Accounts.login_user_by_magic_link(encoded_token)
    360       end
    361     end
    362   end
    363 
    364   describe "delete_user_session_token/1" do
    365     test "deletes the token" do
    366       user = user_fixture()
    367       token = Accounts.generate_user_session_token(user)
    368       assert Accounts.delete_user_session_token(token) == :ok
    369       refute Accounts.get_user_by_session_token(token)
    370     end
    371   end
    372 
    373   describe "deliver_login_instructions/2" do
    374     setup do
    375       %{user: unconfirmed_user_fixture()}
    376     end
    377 
    378     test "sends token through notification", %{user: user} do
    379       token =
    380         extract_user_token(fn url ->
    381           Accounts.deliver_login_instructions(user, url)
    382         end)
    383 
    384       {:ok, token} = Base.url_decode64(token, padding: false)
    385       assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
    386       assert user_token.user_id == user.id
    387       assert user_token.sent_to == user.email
    388       assert user_token.context == "login"
    389     end
    390   end
    391 
    392   describe "inspect/2 for the User module" do
    393     test "does not include password" do
    394       refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
    395     end
    396   end
    397 end