defmodule Aprsme.Accounts.UserTokenTest do use Aprsme.DataCase, async: true import Aprsme.AccountsFixtures alias Aprsme.Accounts.UserToken alias Aprsme.Repo describe "build_session_token/1" do test "returns a binary token and a UserToken struct with context 'session'" do user = user_fixture() {token, user_token} = UserToken.build_session_token(user) assert is_binary(token) assert byte_size(token) == 32 assert user_token.token == token assert user_token.context == "session" assert user_token.user_id == user.id assert is_nil(user_token.sent_to) end test "produces unique tokens each call" do user = user_fixture() {t1, _} = UserToken.build_session_token(user) {t2, _} = UserToken.build_session_token(user) refute t1 == t2 end end describe "verify_session_token_query/1" do test "returns the user when token is valid" do user = user_fixture() {token, user_token} = UserToken.build_session_token(user) Repo.insert!(user_token) {:ok, query} = UserToken.verify_session_token_query(token) loaded = Repo.one(query) assert loaded.id == user.id end test "returns nil for an unknown token" do {:ok, query} = UserToken.verify_session_token_query(:crypto.strong_rand_bytes(32)) assert Repo.one(query) == nil end end describe "build_email_token/2" do test "returns a URL-safe base64 token and a hashed UserToken" do user = user_fixture() {encoded, user_token} = UserToken.build_email_token(user, "confirm") assert is_binary(encoded) assert {:ok, _} = Base.url_decode64(encoded, padding: false) assert user_token.context == "confirm" assert user_token.sent_to == user.email assert user_token.user_id == user.id # The stored token is a SHA-256 digest (32 bytes), not the encoded plaintext. assert byte_size(user_token.token) == 32 refute user_token.token == encoded end test "supports reset_password context" do user = user_fixture() {_token, user_token} = UserToken.build_email_token(user, "reset_password") assert user_token.context == "reset_password" end end describe "verify_email_token_query/2" do test "returns :error for malformed tokens" do assert UserToken.verify_email_token_query("not-base64!", "confirm") == :error end test "returns {:ok, query} that finds the user for a valid token" do user = user_fixture() {encoded, user_token} = UserToken.build_email_token(user, "confirm") Repo.insert!(user_token) {:ok, query} = UserToken.verify_email_token_query(encoded, "confirm") assert Repo.one(query).id == user.id end test "returns a query matching no user for a valid-looking but unknown token" do # Randomly-generated encoded token that doesn't exist in the DB. encoded = 32 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false) {:ok, query} = UserToken.verify_email_token_query(encoded, "confirm") assert Repo.one(query) == nil end end describe "verify_change_email_token_query/2" do test "returns :error for malformed tokens" do assert UserToken.verify_change_email_token_query("not-base64!", "change:new@example.com") == :error end test "accepts a well-formed token for a change: context" do user = user_fixture() # Reuse build_email_token/2 (same hashing) for a realistic token. {encoded, user_token} = UserToken.build_email_token(user, "change:new@example.com") Repo.insert!(user_token) {:ok, query} = UserToken.verify_change_email_token_query(encoded, "change:new@example.com") # verify_change_email_token_query doesn't select :user, it selects tokens. token_row = Repo.one(query) assert token_row assert token_row.context == "change:new@example.com" end end describe "token_and_context_query/2" do test "returns a query scoped by token and context" do user = user_fixture() {token, user_token} = UserToken.build_session_token(user) Repo.insert!(user_token) query = UserToken.token_and_context_query(token, "session") found = Repo.one(query) assert found.token == token assert found.context == "session" end test "returns nothing for mismatched context" do user = user_fixture() {token, user_token} = UserToken.build_session_token(user) Repo.insert!(user_token) assert token |> UserToken.token_and_context_query("confirm") |> Repo.one() == nil end end describe "user_and_contexts_query/2" do setup do user = user_fixture() {_token, session_token} = UserToken.build_session_token(user) {_token, confirm_token} = UserToken.build_email_token(user, "confirm") Repo.insert!(session_token) Repo.insert!(confirm_token) {:ok, user: user} end test "with :all returns every token for the user", %{user: user} do tokens = user |> UserToken.user_and_contexts_query(:all) |> Repo.all() assert length(tokens) == 2 contexts = tokens |> Enum.map(& &1.context) |> Enum.sort() assert contexts == ["confirm", "session"] end test "with a specific context list filters", %{user: user} do tokens = user |> UserToken.user_and_contexts_query(["session"]) |> Repo.all() assert length(tokens) == 1 assert hd(tokens).context == "session" end test "with an unmatched context returns empty", %{user: user} do assert user |> UserToken.user_and_contexts_query(["reset_password"]) |> Repo.all() == [] end end end