daily-tracker

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

user_auth_test.exs (14430B)


      1 defmodule DailyTrackerWeb.UserAuthTest do
      2   use DailyTrackerWeb.ConnCase, async: true
      3 
      4   alias Phoenix.LiveView
      5   alias DailyTracker.Accounts
      6   alias DailyTracker.Accounts.Scope
      7   alias DailyTrackerWeb.UserAuth
      8 
      9   import DailyTracker.AccountsFixtures
     10 
     11   @remember_me_cookie "_daily_tracker_web_user_remember_me"
     12   @remember_me_cookie_max_age 60 * 60 * 24 * 14
     13 
     14   setup %{conn: conn} do
     15     conn =
     16       conn
     17       |> Map.replace!(:secret_key_base, DailyTrackerWeb.Endpoint.config(:secret_key_base))
     18       |> init_test_session(%{})
     19 
     20     %{user: %{user_fixture() | authenticated_at: DateTime.utc_now(:second)}, conn: conn}
     21   end
     22 
     23   describe "log_in_user/3" do
     24     test "stores the user token in the session", %{conn: conn, user: user} do
     25       conn = UserAuth.log_in_user(conn, user)
     26       assert token = get_session(conn, :user_token)
     27       assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
     28       assert redirected_to(conn) == ~p"/"
     29       assert Accounts.get_user_by_session_token(token)
     30     end
     31 
     32     test "clears everything previously stored in the session", %{conn: conn, user: user} do
     33       conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
     34       refute get_session(conn, :to_be_removed)
     35     end
     36 
     37     test "keeps session when re-authenticating", %{conn: conn, user: user} do
     38       conn =
     39         conn
     40         |> assign(:current_scope, Scope.for_user(user))
     41         |> put_session(:to_be_removed, "value")
     42         |> UserAuth.log_in_user(user)
     43 
     44       assert get_session(conn, :to_be_removed)
     45     end
     46 
     47     test "clears session when user does not match when re-authenticating", %{
     48       conn: conn,
     49       user: user
     50     } do
     51       other_user = user_fixture()
     52 
     53       conn =
     54         conn
     55         |> assign(:current_scope, Scope.for_user(other_user))
     56         |> put_session(:to_be_removed, "value")
     57         |> UserAuth.log_in_user(user)
     58 
     59       refute get_session(conn, :to_be_removed)
     60     end
     61 
     62     test "redirects to the configured path", %{conn: conn, user: user} do
     63       conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
     64       assert redirected_to(conn) == "/hello"
     65     end
     66 
     67     test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
     68       conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
     69       assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
     70       assert get_session(conn, :user_remember_me) == true
     71 
     72       assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
     73       assert signed_token != get_session(conn, :user_token)
     74       assert max_age == @remember_me_cookie_max_age
     75     end
     76 
     77     test "redirects to settings when user is already logged in", %{conn: conn, user: user} do
     78       conn =
     79         conn
     80         |> assign(:current_scope, Scope.for_user(user))
     81         |> UserAuth.log_in_user(user)
     82 
     83       assert redirected_to(conn) == ~p"/users/settings"
     84     end
     85 
     86     test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do
     87       conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
     88       assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
     89       assert get_session(conn, :user_remember_me) == true
     90 
     91       conn =
     92         conn
     93         |> recycle()
     94         |> Map.replace!(:secret_key_base, DailyTrackerWeb.Endpoint.config(:secret_key_base))
     95         |> fetch_cookies()
     96         |> init_test_session(%{user_remember_me: true})
     97 
     98       # the conn is already logged in and has the remember_me cookie set,
     99       # now we log in again and even without explicitly setting remember_me,
    100       # the cookie should be set again
    101       conn = conn |> UserAuth.log_in_user(user, %{})
    102       assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
    103       assert signed_token != get_session(conn, :user_token)
    104       assert max_age == @remember_me_cookie_max_age
    105       assert get_session(conn, :user_remember_me) == true
    106     end
    107   end
    108 
    109   describe "logout_user/1" do
    110     test "erases session and cookies", %{conn: conn, user: user} do
    111       user_token = Accounts.generate_user_session_token(user)
    112 
    113       conn =
    114         conn
    115         |> put_session(:user_token, user_token)
    116         |> put_req_cookie(@remember_me_cookie, user_token)
    117         |> fetch_cookies()
    118         |> UserAuth.log_out_user()
    119 
    120       refute get_session(conn, :user_token)
    121       refute conn.cookies[@remember_me_cookie]
    122       assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
    123       assert redirected_to(conn) == ~p"/"
    124       refute Accounts.get_user_by_session_token(user_token)
    125     end
    126 
    127     test "broadcasts to the given live_socket_id", %{conn: conn} do
    128       live_socket_id = "users_sessions:abcdef-token"
    129       DailyTrackerWeb.Endpoint.subscribe(live_socket_id)
    130 
    131       conn
    132       |> put_session(:live_socket_id, live_socket_id)
    133       |> UserAuth.log_out_user()
    134 
    135       assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
    136     end
    137 
    138     test "works even if user is already logged out", %{conn: conn} do
    139       conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
    140       refute get_session(conn, :user_token)
    141       assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
    142       assert redirected_to(conn) == ~p"/"
    143     end
    144   end
    145 
    146   describe "fetch_current_scope_for_user/2" do
    147     test "authenticates user from session", %{conn: conn, user: user} do
    148       user_token = Accounts.generate_user_session_token(user)
    149 
    150       conn =
    151         conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_scope_for_user([])
    152 
    153       assert conn.assigns.current_scope.user.id == user.id
    154       assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
    155       assert get_session(conn, :user_token) == user_token
    156     end
    157 
    158     test "authenticates user from cookies", %{conn: conn, user: user} do
    159       logged_in_conn =
    160         conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
    161 
    162       user_token = logged_in_conn.cookies[@remember_me_cookie]
    163       %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
    164 
    165       conn =
    166         conn
    167         |> put_req_cookie(@remember_me_cookie, signed_token)
    168         |> UserAuth.fetch_current_scope_for_user([])
    169 
    170       assert conn.assigns.current_scope.user.id == user.id
    171       assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
    172       assert get_session(conn, :user_token) == user_token
    173       assert get_session(conn, :user_remember_me)
    174 
    175       assert get_session(conn, :live_socket_id) ==
    176                "users_sessions:#{Base.url_encode64(user_token)}"
    177     end
    178 
    179     test "does not authenticate if data is missing", %{conn: conn, user: user} do
    180       _ = Accounts.generate_user_session_token(user)
    181       conn = UserAuth.fetch_current_scope_for_user(conn, [])
    182       refute get_session(conn, :user_token)
    183       refute conn.assigns.current_scope
    184     end
    185 
    186     test "reissues a new token after a few days and refreshes cookie", %{conn: conn, user: user} do
    187       logged_in_conn =
    188         conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
    189 
    190       token = logged_in_conn.cookies[@remember_me_cookie]
    191       %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
    192 
    193       offset_user_token(token, -10, :day)
    194       {user, _} = Accounts.get_user_by_session_token(token)
    195 
    196       conn =
    197         conn
    198         |> put_session(:user_token, token)
    199         |> put_session(:user_remember_me, true)
    200         |> put_req_cookie(@remember_me_cookie, signed_token)
    201         |> UserAuth.fetch_current_scope_for_user([])
    202 
    203       assert conn.assigns.current_scope.user.id == user.id
    204       assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
    205       assert new_token = get_session(conn, :user_token)
    206       assert new_token != token
    207       assert %{value: new_signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
    208       assert new_signed_token != signed_token
    209       assert max_age == @remember_me_cookie_max_age
    210     end
    211   end
    212 
    213   describe "on_mount :mount_current_scope" do
    214     setup %{conn: conn} do
    215       %{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
    216     end
    217 
    218     test "assigns current_scope based on a valid user_token", %{conn: conn, user: user} do
    219       user_token = Accounts.generate_user_session_token(user)
    220       session = conn |> put_session(:user_token, user_token) |> get_session()
    221 
    222       {:cont, updated_socket} =
    223         UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
    224 
    225       assert updated_socket.assigns.current_scope.user.id == user.id
    226     end
    227 
    228     test "assigns nil to current_scope assign if there isn't a valid user_token", %{conn: conn} do
    229       user_token = "invalid_token"
    230       session = conn |> put_session(:user_token, user_token) |> get_session()
    231 
    232       {:cont, updated_socket} =
    233         UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
    234 
    235       assert updated_socket.assigns.current_scope == nil
    236     end
    237 
    238     test "assigns nil to current_scope assign if there isn't a user_token", %{conn: conn} do
    239       session = conn |> get_session()
    240 
    241       {:cont, updated_socket} =
    242         UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
    243 
    244       assert updated_socket.assigns.current_scope == nil
    245     end
    246   end
    247 
    248   describe "on_mount :require_authenticated" do
    249     test "authenticates current_scope based on a valid user_token", %{conn: conn, user: user} do
    250       user_token = Accounts.generate_user_session_token(user)
    251       session = conn |> put_session(:user_token, user_token) |> get_session()
    252 
    253       {:cont, updated_socket} =
    254         UserAuth.on_mount(:require_authenticated, %{}, session, %LiveView.Socket{})
    255 
    256       assert updated_socket.assigns.current_scope.user.id == user.id
    257     end
    258 
    259     test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
    260       user_token = "invalid_token"
    261       session = conn |> put_session(:user_token, user_token) |> get_session()
    262 
    263       socket = %LiveView.Socket{
    264         endpoint: DailyTrackerWeb.Endpoint,
    265         assigns: %{__changed__: %{}, flash: %{}}
    266       }
    267 
    268       {:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
    269       assert updated_socket.assigns.current_scope == nil
    270     end
    271 
    272     test "redirects to login page if there isn't a user_token", %{conn: conn} do
    273       session = conn |> get_session()
    274 
    275       socket = %LiveView.Socket{
    276         endpoint: DailyTrackerWeb.Endpoint,
    277         assigns: %{__changed__: %{}, flash: %{}}
    278       }
    279 
    280       {:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
    281       assert updated_socket.assigns.current_scope == nil
    282     end
    283   end
    284 
    285   describe "on_mount :require_sudo_mode" do
    286     test "allows users that have authenticated in the last 10 minutes", %{conn: conn, user: user} do
    287       user_token = Accounts.generate_user_session_token(user)
    288       session = conn |> put_session(:user_token, user_token) |> get_session()
    289 
    290       socket = %LiveView.Socket{
    291         endpoint: DailyTrackerWeb.Endpoint,
    292         assigns: %{__changed__: %{}, flash: %{}}
    293       }
    294 
    295       assert {:cont, _updated_socket} =
    296                UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
    297     end
    298 
    299     test "redirects when authentication is too old", %{conn: conn, user: user} do
    300       eleven_minutes_ago = DateTime.utc_now(:second) |> DateTime.add(-11, :minute)
    301       user = %{user | authenticated_at: eleven_minutes_ago}
    302       user_token = Accounts.generate_user_session_token(user)
    303       {user, token_inserted_at} = Accounts.get_user_by_session_token(user_token)
    304       assert DateTime.compare(token_inserted_at, user.authenticated_at) == :gt
    305       session = conn |> put_session(:user_token, user_token) |> get_session()
    306 
    307       socket = %LiveView.Socket{
    308         endpoint: DailyTrackerWeb.Endpoint,
    309         assigns: %{__changed__: %{}, flash: %{}}
    310       }
    311 
    312       assert {:halt, _updated_socket} =
    313                UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
    314     end
    315   end
    316 
    317   describe "require_authenticated_user/2" do
    318     setup %{conn: conn} do
    319       %{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
    320     end
    321 
    322     test "redirects if user is not authenticated", %{conn: conn} do
    323       conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
    324       assert conn.halted
    325 
    326       assert redirected_to(conn) == ~p"/users/log-in"
    327 
    328       assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
    329                "You must log in to access this page."
    330     end
    331 
    332     test "stores the path to redirect to on GET", %{conn: conn} do
    333       halted_conn =
    334         %{conn | path_info: ["foo"], query_string: ""}
    335         |> fetch_flash()
    336         |> UserAuth.require_authenticated_user([])
    337 
    338       assert halted_conn.halted
    339       assert get_session(halted_conn, :user_return_to) == "/foo"
    340 
    341       halted_conn =
    342         %{conn | path_info: ["foo"], query_string: "bar=baz"}
    343         |> fetch_flash()
    344         |> UserAuth.require_authenticated_user([])
    345 
    346       assert halted_conn.halted
    347       assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
    348 
    349       halted_conn =
    350         %{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
    351         |> fetch_flash()
    352         |> UserAuth.require_authenticated_user([])
    353 
    354       assert halted_conn.halted
    355       refute get_session(halted_conn, :user_return_to)
    356     end
    357 
    358     test "does not redirect if user is authenticated", %{conn: conn, user: user} do
    359       conn =
    360         conn
    361         |> assign(:current_scope, Scope.for_user(user))
    362         |> UserAuth.require_authenticated_user([])
    363 
    364       refute conn.halted
    365       refute conn.status
    366     end
    367   end
    368 
    369   describe "disconnect_sessions/1" do
    370     test "broadcasts disconnect messages for each token" do
    371       tokens = [%{token: "token1"}, %{token: "token2"}]
    372 
    373       for %{token: token} <- tokens do
    374         DailyTrackerWeb.Endpoint.subscribe("users_sessions:#{Base.url_encode64(token)}")
    375       end
    376 
    377       UserAuth.disconnect_sessions(tokens)
    378 
    379       assert_receive %Phoenix.Socket.Broadcast{
    380         event: "disconnect",
    381         topic: "users_sessions:dG9rZW4x"
    382       }
    383 
    384       assert_receive %Phoenix.Socket.Broadcast{
    385         event: "disconnect",
    386         topic: "users_sessions:dG9rZW4y"
    387       }
    388     end
    389   end
    390 end