defmodule Microwaveprop.AccountsApiTokenTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Accounts alias Microwaveprop.Accounts.UserApiToken alias Microwaveprop.AccountsFixtures setup do %{user: AccountsFixtures.user_fixture()} end describe "create_api_token/2" do test "returns plaintext + persisted record", %{user: user} do assert {:ok, {plaintext, %UserApiToken{} = record}} = Accounts.create_api_token(user, %{name: "Laptop"}) assert String.starts_with?(plaintext, "mwp_") assert record.user_id == user.id assert record.token_hash == UserApiToken.hash_token(plaintext) assert record.id end test "surfaces validation errors", %{user: user} do assert {:error, %Ecto.Changeset{}} = Accounts.create_api_token(user, %{name: ""}) end test "rejects expires_at in the past", %{user: user} do past = DateTime.add(DateTime.utc_now(), -1, :second) assert {:error, changeset} = Accounts.create_api_token(user, %{name: "x", expires_at: past}) assert %{expires_at: ["must be in the future"]} = errors_on(changeset) end end describe "list_api_tokens/1" do test "returns only non-revoked tokens for the user, newest first", %{user: user} do other = AccountsFixtures.user_fixture() {:ok, {_, t1}} = Accounts.create_api_token(user, %{name: "A"}) {:ok, {_, t2}} = Accounts.create_api_token(user, %{name: "B"}) {:ok, {_, _t3}} = Accounts.create_api_token(other, %{name: "Other"}) {:ok, _} = Accounts.revoke_api_token(user, t1.id) assert [token] = Accounts.list_api_tokens(user) assert token.id == t2.id end end describe "get_user_by_api_token/1" do test "returns user + token + updates last_used_at on valid", %{user: user} do {:ok, {plaintext, original}} = Accounts.create_api_token(user, %{name: "A"}) assert is_nil(original.last_used_at) assert {:ok, returned_user, returned_token} = Accounts.get_user_by_api_token(plaintext) assert returned_user.id == user.id assert returned_token.id == original.id assert %DateTime{} = returned_token.last_used_at end test "rejects unknown plaintext" do assert {:error, :invalid_token} = Accounts.get_user_by_api_token("nope") end test "rejects revoked tokens", %{user: user} do {:ok, {plaintext, record}} = Accounts.create_api_token(user, %{name: "A"}) {:ok, _} = Accounts.revoke_api_token(user, record.id) assert {:error, :invalid_token} = Accounts.get_user_by_api_token(plaintext) end test "rejects expired tokens", %{user: user} do future = DateTime.add(DateTime.utc_now(), 60, :second) {:ok, {plaintext, record}} = Accounts.create_api_token(user, %{name: "A", expires_at: future}) # Force the row past its expiry. Repo.update_all( from(t in UserApiToken, where: t.id == ^record.id), set: [expires_at: DateTime.utc_now() |> DateTime.add(-1, :second) |> DateTime.truncate(:second)] ) assert {:error, :invalid_token} = Accounts.get_user_by_api_token(plaintext) end end describe "revoke_api_token/2" do test "marks the token revoked", %{user: user} do {:ok, {_pt, record}} = Accounts.create_api_token(user, %{name: "x"}) assert {:ok, revoked} = Accounts.revoke_api_token(user, record.id) assert %DateTime{} = revoked.revoked_at end test "is idempotent for already-revoked tokens", %{user: user} do {:ok, {_pt, record}} = Accounts.create_api_token(user, %{name: "x"}) {:ok, first} = Accounts.revoke_api_token(user, record.id) {:ok, second} = Accounts.revoke_api_token(user, record.id) assert DateTime.compare(first.revoked_at, second.revoked_at) == :eq end test "returns :not_found for tokens owned by other users", %{user: user} do other = AccountsFixtures.user_fixture() {:ok, {_pt, record}} = Accounts.create_api_token(user, %{name: "x"}) assert {:error, :not_found} = Accounts.revoke_api_token(other, record.id) end test "returns :not_found for a malformed UUID", %{user: user} do assert {:error, :not_found} = Accounts.revoke_api_token(user, "not-a-uuid") end end end