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