diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b565ee7..fef8e427 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,14 +47,38 @@ jobs: env: UV_PYTHON: ${{ matrix.python-version }} + crosshair: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + fetch-depth: 0 + fetch-tags: true + + - name: Install uv + uses: astral-sh/setup-uv@v10.1.0 + with: + enable-cache: true + cache-dependency-glob: '**/pyproject.toml' + + - name: Run property tests with CrossHair + run: | + uv run --group=dev --group=crosshair pytest -vvv tests/test_vws_auth_tools.py + env: + HYPOTHESIS_BACKEND: crosshair + UV_PYTHON: '3.14' + completion-ci: - needs: build + needs: [build, crosshair] runs-on: ubuntu-latest if: always() # Run even if one matrix job fails steps: - name: Check matrix job status run: |- - if ! ${{ needs.build.result == 'success' }}; then + if ! ${{ needs.build.result == 'success' && needs.crosshair.result == 'success' }}; then echo "One or more matrix jobs failed" exit 1 fi diff --git a/pyproject.toml b/pyproject.toml index d2da6e06..67e04af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,10 @@ dev = [ "yamlfix==1.19.1", "zizmor==1.30.1", ] +crosshair = [ + "crosshair-tool==0.0.110", + "hypothesis-crosshair==0.0.30", +] release = [ "check-wheel-contents==0.6.3", "towncrier==26.9.0" ] [tool.setuptools] diff --git a/tests/test_vws_auth_tools.py b/tests/test_vws_auth_tools.py index c12fbe81..134aeb47 100644 --- a/tests/test_vws_auth_tools.py +++ b/tests/test_vws_auth_tools.py @@ -3,15 +3,21 @@ import base64 import datetime import hashlib +import os from zoneinfo import ZoneInfo import pytest from freezegun import freeze_time -from hypothesis import given +from hypothesis import given, settings from hypothesis import strategies as st import vws_auth_tools +_HYPOTHESIS_BACKEND = os.environ.get( + key="HYPOTHESIS_BACKEND", + default="hypothesis", +) + def test_rfc_1123_date() -> None: """The date is returned in the format described in the VWS @@ -103,6 +109,7 @@ def test_authorization_header_empty_content( assert result == "VWS my_access_key:XXvKyRyMkwS8/1P1WLQ0duqNpKs=" +@settings(backend=_HYPOTHESIS_BACKEND, deadline=None) @given( access_key=st.text(), secret_key=st.text(), @@ -170,6 +177,7 @@ def test_basic_authorization_header_empty_credentials() -> None: assert result == "Basic Og==" +@settings(backend=_HYPOTHESIS_BACKEND) @given(client_id=st.text(), client_secret=st.text()) def test_basic_authorization_header_encodes_utf_8( *, @@ -191,6 +199,7 @@ def test_basic_authorization_header_encodes_utf_8( assert decoded_credentials == f"{client_id}:{client_secret}" +@settings(backend=_HYPOTHESIS_BACKEND) @given(access_token=st.text()) def test_bearer_authorization_header(access_token: str) -> None: """The Bearer Authorization header includes the given access token.""" @@ -201,6 +210,7 @@ def test_bearer_authorization_header(access_token: str) -> None: assert result == f"Bearer {access_token}" +@settings(backend=_HYPOTHESIS_BACKEND) @given(content=st.text()) def test_authorization_header_encodes_unicode_content(content: str) -> None: """Unicode content is equivalent to its UTF-8 encoded bytes."""