1

Тема: юніт тест на pytest для FastAPI апки

Всім привіт, пробую писати юніт тести на pytest для апки FastAPI і стикнувся з незрозумілою проблемою - отримую невірний код відповіді коли не авторизований.

(якшо шо, то інший юніт тест працює коректно)

функціонал для авторизації:

@app.post("/login")
async def login(request: Request, token: str = Form(...), db: Session = Depends(get_db)):
    
    logging.basicConfig(level = logging.INFO)
    logging.info("post /login")

    users = get_users(db)

    if users:
        logging.debug(f"debug /login - users count: {len(users)}")
    else:
        logging.debug(f"debug /login - Error! users is null")

    authenticated_user = None
    for user in users:
        if pbkdf2_sha256.verify(token, user.token):
            authenticated_user = user
            break

    if authenticated_user:
        user_description = authenticated_user.description
        logging.info('debug 1111')
        #data = {"logout_url": app.url_path_for('logout'),}
        data = {
                "checkmark_url": app.url_path_for('checkmark'),
                "checkmark_negative_url": app.url_path_for('checkmark_negative'),
                "api_requests_url": app.url_path_for('api_requests'),
                "mapping_url": app.url_path_for('mapping'),
                "schedule_list_url": app.url_path_for('schedule_list'),
                "fetch_data_to_csv_route_url": app.url_path_for('fetch_data_to_csv_route'),
                "datasource_url": app.url_path_for('datasource'),
                "secret_tokens_url": app.url_path_for('secret_tokens'),
                "home_url": app.url_path_for('home'),
                "logout_url": app.url_path_for('logout'),
        }
        response = templates.TemplateResponse('dashboard.html', {
            "request": request, 
            "description": user_description, 
            "current_user_id": authenticated_user.id, 
            "current_user_isadmin": authenticated_user.isadmin, 
            "current_user": authenticated_user, 
            "data": data
        }), 200
        #return response
        logging.info('debug 1114')
        user_data = {"sub": "token", "id": authenticated_user.id, "description": user_description, "isadmin": authenticated_user.isadmin, "scopes": ["read", "write"]}
        token_jwt = create_jwt_token(user_data)
        response.set_cookie("token", token_jwt)
        logging.info('debug 1115')

        return response
    else:
        response = templates.TemplateResponse('login.html', {
            "request": request, 
            "messages": messages
        }), 401
        return response

@app.get("/login", name="login", response_class=HTMLResponse)
def login_form(request: Request, next: str = "", current_user: SecretToken = Depends(get_current_user)):
    if current_user is not None:
        logging.info('debug login get - current_user is not None')
        if next:
            response = RedirectResponse(url=next)
        else:
            data = {"logout_url": app.url_path_for('logout'),}
            response = templates.TemplateResponse('dashboard.html', {"request": request, "current_user": current_user, "data": data})
        return response
    else:
        messages = ['please authenticate']
        logging.basicConfig(level = logging.INFO)
        logging.info("get /login")
        logging.info('debug login get - current_user is None')
        logging.info(f'debug login get - request: {str(request)}')
        now = datetime.now()
        logging.info(f'debug login get - now: {now}')
        return templates.TemplateResponse('login.html', {"request": request, "messages": messages}), 401

сам юніт тест:

@pytest.mark.asyncio
async def test_login_wrong_token(client):
    """
    Test if the login view returns the correct response (401 Unauthorized)
    when an incorrect token is provided.
    """
    rv = client.post('/login', data={'token': 'wrong_token'})
    assert rv.status_code == 401, f"Expected status code 401, but got {rv.status_code}"
    print("Test passed: The login view returned the correct response (401 Unauthorized) when an incorrect token was provided.")

результат:

mycode1   | ============================= test session starts ==============================
mycode1   | platform linux -- Python 3.10.11, pytest-8.0.0, pluggy-1.4.0
mycode1   | rootdir: /var/www/app
mycode1   | plugins: anyio-4.2.0, asyncio-0.23.5
mycode1   | asyncio: mode=strict
mycode1   | collected 5 items
mycode1   |
mycode1   | test_app.py FF.FF                                                        [100%]
mycode1   |
mycode1   | =================================== FAILURES ===================================
mycode1   | ____________________________ test_login_wrong_token ____________________________
mycode1   |
mycode1   | client = <starlette.testclient.TestClient object at 0x7fcedb6abc70>
mycode1   |
mycode1   |     @pytest.mark.asyncio
mycode1   |     async def test_login_wrong_token(client):
mycode1   |         """
mycode1   |         Test if the login view returns the correct response (401 Unauthorized)
mycode1   |         when an incorrect token is provided.
mycode1   |         """
mycode1   |         rv = client.post('/login', data={'token': 'wrong_token'})
mycode1   | >       assert rv.status_code == 401, f"Expected status code 401, but got {rv.status_code}"
mycode1   | E       AssertionError: Expected status code 401, but got 200
mycode1   | E       assert 200 == 401
mycode1   | E        +  where 200 = <Response [200 OK]>.status_code
mycode1   |
mycode1   | test_app.py:61: AssertionError

Повинна повертатися помилка з кодом 401, а повертається чомусь 200 (ок).
Не розумію чому. Може комусь збоку явно видно де моя помилка.

2

Re: юніт тест на pytest для FastAPI апки

Тест містить client.post, то на @app.get я не дивлю, тільки в @app.post.
Щоб оторимати 401, треба попасти в else, для цього треба щоб для всіх кориистувачів pbkdf2_sha256.verify(token, user.token) вернув false. Якщо verify для когось вертає true, то тут (або) :

  • База (get_users(db)) містить 'wrong_token', user'ів треба перевірити і почистити.

  • Функція pbkdf2_sha256.verify працює не очікувано, можна замінити бібліотеку.

frz написав:
for user in users:

Ну і сама ідея, циклом перебирати всіх користувачів щоб пройти аутентикацію для одного з них, це варто замінити на шось простіше [для машини]. В ідеалі сам запит до бази має містити фільтри по login і token.

Прихований текст

Код подібний на наслідок роботи з GPT. Завязуйте з ним.

Відступи в data можна пофіксити. І спільний return response можна винести. Але то не критично.

Ще name="login" я би поклав в post теж.

Подякували: koala1

3 Востаннє редагувалося frz (13.02.2024 10:34:55)

Re: юніт тест на pytest для FastAPI апки

leofun01 написав:

циклом перебирати всіх користувачів щоб пройти аутентикацію для одного з них

Це тому що на сервері апі аутентифікація лише за токеном. Не існує пари логін-пароль. Мій функціонал тут не може стрибнути вище функціоналу самого сервера апі, оскільки використовується той самий токен. За перфоменсом це може бути проблемою, якщо кількість користувачів буде великою, однак за архітектурою передбачається, що тулза буде деплоїтись на окремі інстанси для різних груп користувачів (їхні власні хмари), тобто в базі цього інстансу буде лише декілька токенів.

Також все працювало коректно, поки функціонал був на Flask, та оскільки цей фреймворк не асинхронний, то змігрував на FastAPI і тепер чомусь цей юніт-тест перестав проходити. Копатиму далі.