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