"""Task 2: `Product` tenancy -- API key hashing/verification (`tenancy.service`) and the `require_product` FastAPI dependency (`tenancy.deps`), the adaptation of the auto's `require_permission(...)` (porte table: API key -> Product, no JWT/user here). Every key below is generated with a `uuid4` suffix (`_key()`), never a fixed literal: `products` has NO tenant scoping to isolate one test's rows from another's (unlike the auto's org-scoped tables, where a fixed literal is safe because every query also filters by that test's own `organization_id`) -- `authenticate_product` deliberately scans ALL live products (see its docstring), so a fixed key reused across two tests would have the OLDER, still-committed product (this suite's `db_session` fixture only rolls back; it does not truncate what earlier tests committed) answer for the newer test's assertions instead of a genuine 404/401.""" import uuid from datetime import datetime, timezone import pytest from fastapi import HTTPException from fiscal_svc.tenancy.deps import require_product from fiscal_svc.tenancy.service import authenticate_product, create_product, hash_api_key def _key() -> str: return f"k-{uuid.uuid4().hex}" @pytest.mark.asyncio async def test_create_product_never_persists_the_plaintext_key(db_session): key = _key() product = await create_product(db_session, name="auto", api_key=key) assert product.api_key_hash != key # bcrypt's own marker -- proves passlib actually hashed it, not just # stored some other opaque transform. assert product.api_key_hash.startswith("$2b$") @pytest.mark.asyncio async def test_authenticate_product_returns_product_for_the_correct_key(db_session): key_a, key_b = _key(), _key() product = await create_product(db_session, name="auto", api_key=key_a) await create_product(db_session, name="crm", api_key=key_b) found = await authenticate_product(db_session, key_a) assert found is not None assert found.id == product.id @pytest.mark.asyncio async def test_authenticate_product_returns_none_for_a_wrong_key(db_session): await create_product(db_session, name="auto", api_key=_key()) assert await authenticate_product(db_session, _key()) is None @pytest.mark.asyncio async def test_authenticate_product_ignores_soft_deleted_products(db_session): """A revoked/deactivated Product's key must stop authenticating -- same 'revoked = gone' semantics as the auto's `get_active_user` ignoring a soft-deleted User.""" key = _key() product = await create_product(db_session, name="auto", api_key=key) product.deleted_at = datetime.now(timezone.utc) await db_session.commit() assert await authenticate_product(db_session, key) is None @pytest.mark.asyncio async def test_authenticate_product_distinguishes_products_with_similar_keys(db_session): """Not a narrowed lookup (no identifier besides the key itself) -- proves the "verify against every live product" loop in `authenticate_product` returns the RIGHT product, not just A product.""" key_a, key_b = _key(), _key() product_a = await create_product(db_session, name="auto", api_key=key_a) product_b = await create_product(db_session, name="crm", api_key=key_b) found_a = await authenticate_product(db_session, key_a) found_b = await authenticate_product(db_session, key_b) assert found_a.id == product_a.id assert found_b.id == product_b.id @pytest.mark.asyncio async def test_require_product_missing_header_is_401(db_session): with pytest.raises(HTTPException) as exc_info: await require_product(x_api_key=None, session=db_session) assert exc_info.value.status_code == 401 @pytest.mark.asyncio async def test_require_product_wrong_key_is_401(db_session): key = _key() await create_product(db_session, name="auto", api_key=key) with pytest.raises(HTTPException) as exc_info: await require_product(x_api_key=f"not-{key}", session=db_session) assert exc_info.value.status_code == 401 @pytest.mark.asyncio async def test_require_product_soft_deleted_product_is_401(db_session): key = _key() product = await create_product(db_session, name="auto", api_key=key) product.deleted_at = datetime.now(timezone.utc) await db_session.commit() with pytest.raises(HTTPException) as exc_info: await require_product(x_api_key=key, session=db_session) assert exc_info.value.status_code == 401 @pytest.mark.asyncio async def test_require_product_valid_key_returns_the_product(db_session): key = _key() product = await create_product(db_session, name="auto", api_key=key) result = await require_product(x_api_key=key, session=db_session) assert result.id == product.id def test_hash_api_key_is_salted_non_deterministic(): """Same input, two different hashes -- this is EXACTLY why `authenticate_product` cannot do `WHERE api_key_hash = hash(candidate)` and must verify against each live product's hash instead (see its docstring).""" same_input = _key() assert hash_api_key(same_input) != hash_api_key(same_input)