import pytest import app.routes.auth as auth_module @pytest.fixture(autouse=True) def _reset_guest_limiter(): # The limiter is module-level state; a previous test's hits would bleed # into the next one's per-IP budget. auth_module.guest_limiter._hits.clear() yield @pytest.mark.asyncio async def test_guest_creates_wanderer_and_cookie_works_on_me(client): response = await client.post("/auth/guest") assert response.status_code == 201 body = response.json() assert body["username"].startswith("wanderer-") assert "password" not in body assert "qm_session" in response.cookies me_resp = await client.get("/auth/me") assert me_resp.status_code == 200 assert me_resp.json()["username"] == body["username"] @pytest.mark.asyncio async def test_guest_cannot_login_with_any_password(client): response = await client.post("/auth/guest") username = response.json()["username"] login_resp = await client.post( "/auth/login", json={"username": username, "password": "anythingatall"} ) assert login_resp.status_code == 401 @pytest.mark.asyncio async def test_guest_rate_limit_fires_per_ip(client): for _ in range(5): response = await client.post("/auth/guest") assert response.status_code == 201 response = await client.post("/auth/guest") assert response.status_code == 429 @pytest.mark.asyncio async def test_guest_username_collision_retries(client, monkeypatch): taken_resp = await client.post("/auth/guest") taken = taken_resp.json()["username"].removeprefix("wanderer-") # First attempt collides with the existing wanderer; the retry must land # on the fresh suffix instead of erroring out. suffixes = iter([taken, "f4ee"]) monkeypatch.setattr( auth_module.secrets, "token_hex", lambda n: next(suffixes) ) response = await client.post("/auth/guest") assert response.status_code == 201 assert response.json()["username"] == "wanderer-f4ee" @pytest.mark.asyncio async def test_registering_as_a_wanderer_claims_the_same_account_in_place(client, db_session): """The whole point of 'claim a name to keep your codex': registering while a guest must upgrade the SAME row, not abandon it for a new one. Proven by crediting essence to the guest first, then checking it survives registration under the guest's still-valid cookie.""" from sqlalchemy import select from app.inventory import credit_essence from app.models.user import User guest_resp = await client.post("/auth/guest") guest_id = guest_resp.json()["id"] guest_username = guest_resp.json()["username"] user = await db_session.get(User, guest_id) credit_essence(user, 37) await db_session.commit() register_resp = await client.post( "/auth/register", json={"username": "claimedname", "password": "spookyspooky1"} ) assert register_resp.status_code == 201 body = register_resp.json() # Same id, same essence — this is an upgrade, not a fresh account. assert body["id"] == guest_id assert body["username"] == "claimedname" assert body["essence"] == 37 # The old wanderer username no longer resolves to a row at all — it was # renamed in place, not duplicated. old = await db_session.scalar(select(User).where(User.username == guest_username)) assert old is None # The guest's original cookie is still a valid session for this same, # now-named, account — claiming a name doesn't log you out. me_resp = await client.get("/auth/me") assert me_resp.status_code == 200 assert me_resp.json()["id"] == guest_id assert me_resp.json()["username"] == "claimedname" # And the new credentials actually work, for a future login elsewhere. login_resp = await client.post( "/auth/login", json={"username": "claimedname", "password": "spookyspooky1"} ) assert login_resp.status_code == 200 assert login_resp.json()["id"] == guest_id @pytest.mark.asyncio async def test_registering_while_already_named_creates_a_separate_account(client, db_session): """Registering a second account while logged in is legitimate (shared computer, alt account), so a named caller's cookie is ignored and a genuinely new row is created — the in-place claim is for wanderers only. It must NOT rename the logged-in account out from under them.""" first = await client.post( "/auth/register", json={"username": "alreadynamed", "password": "spookyspooky1"} ) first_id = first.json()["id"] await client.post( "/auth/login", json={"username": "alreadynamed", "password": "spookyspooky1"} ) second = await client.post( "/auth/register", json={"username": "somethingelse", "password": "spookyspooky2"} ) assert second.status_code == 201 assert second.json()["id"] != first_id # The original account still exists under its own name, untouched. from app.models.user import User still_there = await db_session.get(User, first_id) assert still_there is not None assert still_there.username == "alreadynamed" @pytest.mark.asyncio async def test_anonymous_registration_is_unaffected(client): # No cookie at all — the original, pre-guest behaviour must be intact. resp = await client.post( "/auth/register", json={"username": "freshaccount", "password": "spookyspooky1"} ) assert resp.status_code == 201 assert resp.json()["username"] == "freshaccount"