fastapi / full-stack-fastapi-template

Full stack, modern web application template. Using FastAPI, React, SQLModel, PostgreSQL, Docker, GitHub Actions, automatic HTTPS and more.
MIT License
26.8k stars 4.74k forks source link

How can I override my JWT auth dependency in the endponits in FastAPI testing? #635

Open SebaRossi94 opened 7 months ago

SebaRossi94 commented 7 months ago

I managed to override my DB dependency for my testing in FastAPI but when I try to apply the same technique for overriding my JWT auth dependency I still get a 422 response with the following detail:

{'detail': [{'type': 'missing', 'loc': ['query', 'fake_jwt'], 'msg': 'Field required', 'input': None, 'url': 'https://errors.pydantic.dev/2.6/v/missing'}]}

I followed all documentation and suggested posts on this topic of overriding dependencies for testing and still haven't found my issue. Does anyone know what's the problem here? Here are the involved codes:

conftest.py

def init_test_db(_app):
    engine = create_engine(
        settings.sql_alchemy_database_url,
        connect_args={"check_same_thread": False},
    )

    def override_get_session():
        with Session(engine, autoflush=True) as session:
            with session.begin():
                yield session

    def override_get_session_no_transaction():
        with Session(engine) as session:
            yield session

    _app.dependency_overrides[get_session] = override_get_session
    _app.dependency_overrides[get_session_no_transaction] = (
        override_get_session_no_transaction
    )
    SQLBaseModel.metadata.create_all(bind=engine)
    with Session(engine) as session:
        for user in fake_users.values():
            session.add(user)
        session.commit()
    return engine

@pytest.fixture
def app_with_db():
    from app.main import app

    test_engine = init_test_db(app)
    yield app
    app.dependency_overrides = {}
    drop_test_db(test_engine)

@pytest.fixture()
def app_with_db_and_jwt(app_with_db):
    def override_jwt_dependency(fake_jwt):
        print(fake_jwt)
        return TokenData(id=1, email="jointhedarkside@empire.com")
    app_with_db.dependency_overrides[validate_access_token] = override_jwt_dependency
    yield app_with_db

test.py

def test_me_success(app_with_db_and_jwt):
    darth_user = fake_users["darth"]
    client = TestClient(app_with_db_and_jwt)
    headers = {"Authorization": "Bearer fakejwt.super.fake"}
    me_response = client.get("/users/me", headers=headers)
    print(me_response.json())
    assert me_response.status_code == 200

schemas.py

from pydantic import BaseModel

class TokenSchema(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    id: int
    email: str

router.py

@users_router.get("/me", response_model=ResponseUserSchema)
def me(user_data: jwt_dependency, db: get_session_dependency):
    user = db.exec(
        select(User).where(User.email == user_data.email, User.id == user_data.id)
    ).first()
    return user

dependencies.py

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

token_dependency = Annotated[str, Depends(oauth2_scheme)]
def validate_access_token(token: token_dependency):
    try:
        token_payload = jwt.decode(
            token, key=settings.jwt_secret_key, algorithms=settings.jwt_algorithm
        )
        email = token_payload.get("sub")
        user_id = token_payload.get("id")
        if email is None or user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate user",
            )
        else:
            return TokenData(id=user_id, email=email)
    except JWTError as e:
        logger.logger.exception(e)
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate user"
        )

jwt_dependency = Annotated[TokenData, Depends(validate_access_token)]
saltie2193 commented 1 day ago

In case you did not find a solution yet.

The problem might be, that the override override_jwt_dependency has one argument fake_jwt. This should be the same as adding it this argument in the me function, resulting in an expected query parameter fake_jwt. (.../api/me?fake_jwt=<some value>).

Using a function without any arguments as override however should work:

# conftest.py
...
@pytest.fixture()
def app_with_db_and_jwt(app_with_db):
    def override_jwt_dependency():
        return TokenData(id=1, email="jointhedarkside@empire.com")
    app_with_db.dependency_overrides[validate_access_token] = override_jwt_dependency
    yield app_with_db
    app_with_db.dependency_overrides.pop(validate_access_token) # remove override