kevin1024 / vcrpy

Automatically mock your HTTP interactions to simplify and speed up testing

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

httpx auth flow is skipped by replay?

Ogaday opened this issue · comments

commented

Hello, firstly thanks for VCR - I use it tons of projects. I've come across an issue with VCR for custom authorization with HTTPX.

I'm following the custom auth instructions for HTTPX here and testing my implementation with vcrpy.

I've tried to simplify as much as possible:

import httpx

class MyAuth(httpx.Auth):
    requires_response_body = True

    def __init__(self, client_id: str, client_secret: str, auth_url: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.auth_url = auth_url
        self.access_token = "Bearer empty"

    def auth_flow(self, request):
        try:
            request.headers["Authorization"] = self.access_token
            response = yield request
            response.raise_for_status()
        except httpx.HTTPStatusError:
            auth_response = yield self.build_refresh_request()
            self.update_token(response=auth_response)
            request.headers["Authorization"] = self.access_token
            yield request

    def build_refresh_request(self):
        return httpx.Request(
            "POST",
            self.auth_url,
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
            }
        )

    def update_token(self, response: httpx.Response):
        contents = response.json()
        self.access_token = f"Bearer {contents['access_token']}"

And test file:

import os
from typing import Dict

import httpx
import pytest
import vcr

from my_auth import MyAuth


@pytest.fixture
def secrets() -> Dict[str, str]:
    return {
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],
        "auth_url": os.environ["AUTH_URL"],
        "api_url": os.environ["API_URL"],
        "api_key": os.environ["API_KEY"],
    }


def test_auth(secrets: Dict[str, str]):
    api_url = secrets["api_url"]
    api_key = secrets["api_key"]

    auth = MyAuth(
        client_id=secrets["client_id"],
        client_secret=secrets["client_secret"],
        auth_url=secrets["auth_url"],
    )

    with vcr.use_cassette("my_auth.yaml"):
        with httpx.Client(base_url=api_url) as client:
            r = client.get(
                "/endpoint",
                auth=auth,
                headers={"api-key": api_key, "Content-Type": "application/json"}
            )

    assert r.status_code == 200
    # On first run:
    # test_my_auth.py::test_auth PASSED
    # On subsequent runs:
    # FAILED test_my_auth.py::test_auth - assert 401 == 200

This test runs successfully the first time, as the auth flow is successful, but fails on subsequent test runs. Primarily, it looks like the requests from the auth flow are not replayed from the cassette. I think the mocking of client.get obscures the calling of the auth flow. This means the initial unauthorized response is returned, and there is no auth refresh request made.

This is with:

Python 3.10.8
httpx==0.23.3
pytest==7.2.2

If I could be pointed in the right direction I'd be happy to try to open a PR.