├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── README.md ├── action.yml └── apps └── python ├── app.py ├── hooks.py ├── requirements.in ├── requirements.txt └── secret.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Stranger6667 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | example: 5 | runs-on: ubuntu-22.04 6 | name: Test 7 | steps: 8 | # Gets a copy of the source code in your repository before running API tests 9 | - uses: actions/checkout@v4.2.2 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: '3.12' 13 | 14 | # Installs project's dependencies 15 | - run: pip install -r apps/python/requirements.txt 16 | 17 | # Start the API in the background 18 | - run: python apps/python/app.py & 19 | 20 | # Set sample access token 21 | - name: Set access token 22 | run: cat apps/python/secret.json | python3 -c "import sys, json; print(f'ACCESS_TOKEN={json.load(sys.stdin)[\"access_token\"]}')" >> $GITHUB_ENV 23 | 24 | - name: Default test 25 | uses: ./ 26 | with: 27 | schema: 'http://127.0.0.1:5001/openapi.json' 28 | token: ${{ secrets.SCHEMATHESIS_TOKEN }} 29 | args: '-E success -H "Authorization: Bearer ${{ env.ACCESS_TOKEN }}"' 30 | 31 | - name: Custom hooks 32 | uses: ./ 33 | with: 34 | schema: 'http://127.0.0.1:5001/openapi.json' 35 | token: ${{ secrets.SCHEMATHESIS_TOKEN }} 36 | version: 'latest' 37 | hooks: 'apps.python.hooks' 38 | args: '-c custom_check -E success -H "Authorization: Bearer ${{ env.ACCESS_TOKEN }}"' 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schemathesis GitHub Action 2 | 3 | A GitHub Action for running [Schemathesis](https://github.com/schemathesis/schemathesis) API tests. Automate your API testing to catch crashes, validate specs, and save time. 4 | 5 | ```yaml 6 | - uses: schemathesis/action@v1 7 | with: 8 | # API schema location 9 | schema: 'https://example.schemathesis.io/openapi.json' 10 | ``` 11 | 12 | ## Configuration 13 | 14 | ```yaml 15 | - uses: schemathesis/action@v1 16 | with: 17 | # API schema location 18 | schema: 'https://example.schemathesis.io/openapi.json' 19 | # OPTIONAL. URL that will be used as a prefix for all API operations. 20 | # Useful when the API schema is maintained separately from the application. 21 | base-url: 'https://example.schemathesis.io/v2/' 22 | # OPTIONAL. List of Schemathesis checks to run. Defaults to `all` 23 | checks: 'not_a_server_error' 24 | # OPTIONAL. Maximum time in seconds to wait on the API schema availability 25 | wait-for-schema: '30' 26 | # OPTIONAL. Maximum number of generated examples for each endpoint 27 | max-examples: 50 28 | # OPTIONAL. Specify which version of Schemathesis should be used. 29 | # Defaults to `latest` 30 | version: 'latest' 31 | # OPTIONAL. Schemathesis hooks module. Available for Schemathesis >= 3.18.5 only 32 | hooks: 'tests.hooks' 33 | # OPTIONAL. Extra arguments to pass to Schemathesis 34 | args: '-D negative' 35 | ``` 36 | 37 | To add headers like `Authorization`: 38 | 39 | ```yaml 40 | # Save access token to $GITHUB_ENV as ACCESS_TOKEN. 41 | - name: Set access token 42 | run: echo "ACCESS_TOKEN=super-secret" >> $GITHUB_ENV 43 | 44 | - uses: schemathesis/action@v1 45 | with: 46 | schema: 'http://example.com/api/openapi.json' 47 | args: '-H "Authorization: Bearer ${{ env.ACCESS_TOKEN }}"' 48 | ``` 49 | 50 | For more options and usage, check the [Schemathesis CLI documentation](https://schemathesis.readthedocs.io/en/stable/cli.html). 51 | 52 | ## Support 53 | 54 | Having issues or questions? Check the [Schemathesis documentation](https://schemathesis.readthedocs.io/en/stable/), join the [Discord community](https://discord.gg/R9ASRAmHnA), or report problems on the [GitHub issue tracker](https://github.com/schemathesis/action/issues). 55 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Schemathesis' 2 | description: 'Run Schemathesis API testing' 3 | author: 'Dmitry Dygalo <@Stranger6667> | Schemathesis.io' 4 | branding: 5 | icon: 'check-square' 6 | color: 'green' 7 | inputs: 8 | schema: 9 | description: 'API schema location' 10 | required: true 11 | base-url: 12 | description: 'Base URL address of the API' 13 | required: false 14 | api-name: 15 | description: | 16 | ~~Deprecated~~ 17 | API name from Schemathesis.io. No-op: this input is ignored. 18 | required: false 19 | deprecationMessage: 'Input `api-name` is deprecated and will be removed in the next major release.' 20 | checks: 21 | description: 'List of Schemathesis checks to run. Defaults to `all`' 22 | required: false 23 | default: 'all' 24 | report: 25 | description: | 26 | ~~Deprecated~~ 27 | Whether to send results to Schemathesis.io Web UI. No-op: this input is ignored. 28 | required: false 29 | default: 'true' 30 | deprecationMessage: 'Input `report` is deprecated and will be removed in the next major release.' 31 | token: 32 | description: | 33 | ~~Deprecated~~ 34 | Schemathesis.io API token. No-op: this input is ignored. 35 | required: false 36 | deprecationMessage: 'Input `token` is deprecated and will be removed in the next major release.' 37 | wait-for-schema: 38 | description: 'Maximum time in seconds to wait on the API schema availability' 39 | required: false 40 | default: '2' 41 | max-examples: 42 | description: 'Maximum number of generated examples per each endpoint' 43 | required: false 44 | default: '100' 45 | version: 46 | description: 'Specify which version of Schemathesis should be used. Defaults to `latest`' 47 | required: false 48 | default: 'latest' 49 | hooks: 50 | description: 'Schemathesis hooks module' 51 | required: false 52 | args: 53 | description: 'Extra arguments to pass to Schemathesis' 54 | required: false 55 | runs: 56 | using: 'composite' 57 | steps: 58 | - uses: actions/setup-python@v5 59 | with: 60 | python-version: '3.12' 61 | - uses: astral-sh/setup-uv@v6 62 | with: 63 | version: ">=0.5.0" 64 | - run: | 65 | if [ ${{ inputs.version }} == "latest" ] ; then 66 | uv tool install schemathesis 67 | else 68 | uv tool install schemathesis==${{ inputs.version }} 69 | fi 70 | shell: bash 71 | - run: | 72 | schemathesis run \ 73 | ${{ inputs.schema }} \ 74 | --hypothesis-database=:memory: \ 75 | --hypothesis-max-examples=${{ inputs.max-examples }} \ 76 | --checks=${{ inputs.checks }} \ 77 | ${{ inputs.args }} \ 78 | shell: bash 79 | env: 80 | SCHEMATHESIS_BASE_URL: ${{ inputs.base-url }} 81 | SCHEMATHESIS_WAIT_FOR_SCHEMA: ${{ inputs.wait-for-schema }} 82 | SCHEMATHESIS_HOOKS: ${{ inputs.hooks }} 83 | SCHEMATHESIS_ACTION_REF: ${{ github.action_ref }} 84 | -------------------------------------------------------------------------------- /apps/python/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | from flask import Flask, request 4 | from werkzeug.exceptions import InternalServerError 5 | 6 | app = Flask("test_app") 7 | 8 | HERE = pathlib.Path(__file__).parent.absolute() 9 | 10 | with (HERE / "secret.json").open() as fd: 11 | SECRET_DATA = json.load(fd) 12 | 13 | 14 | ACCESS_TOKEN = SECRET_DATA["access_token"] 15 | 16 | 17 | @app.route("/openapi.json") 18 | def schema(): 19 | return { 20 | "openapi": "3.0.2", 21 | "info": { 22 | "title": "Example API", 23 | "description": "An API to test Schemathesis Action", 24 | "version": "1.0.0", 25 | }, 26 | "paths": { 27 | "/success": { 28 | "get": { 29 | "responses": { 30 | "200": { 31 | "description": "OK", 32 | "content": { 33 | "application/json": { 34 | "schema": { 35 | "type": "object", 36 | "properties": {"success": {"type": "boolean"}}, 37 | "required": ["success"], 38 | } 39 | } 40 | }, 41 | } 42 | } 43 | } 44 | }, 45 | "/failure": {"get": {"responses": {"200": {"description": "OK"}}}}, 46 | }, 47 | "servers": [ 48 | { 49 | "url": "https://example.schemathesis.io/{basePath}", 50 | "variables": {"basePath": {"default": "api"}}, 51 | } 52 | ], 53 | } 54 | 55 | 56 | @app.route("/api/success", methods=["GET"]) 57 | def success(): 58 | if "Authorization" in request.headers and request.headers["Authorization"] == f"Bearer {ACCESS_TOKEN}": 59 | return {"success": True} 60 | return {"detail": "Unauthorized"}, 401 61 | 62 | 63 | @app.route("/api/failure", methods=["GET"]) 64 | def failure(): 65 | raise InternalServerError 66 | 67 | 68 | if __name__ == "__main__": 69 | app.run(host="0.0.0.0", port=5001) 70 | -------------------------------------------------------------------------------- /apps/python/hooks.py: -------------------------------------------------------------------------------- 1 | import schemathesis 2 | 3 | 4 | @schemathesis.check 5 | def custom_check(response, case): 6 | # Always succeeds 7 | return None 8 | -------------------------------------------------------------------------------- /apps/python/requirements.in: -------------------------------------------------------------------------------- 1 | Flask -------------------------------------------------------------------------------- /apps/python/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.10 3 | # To update, run: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | click==8.1.3 8 | # via flask 9 | flask==2.1.3 10 | # via -r requirements.in 11 | itsdangerous==2.1.2 12 | # via flask 13 | jinja2==3.1.2 14 | # via flask 15 | markupsafe==2.1.1 16 | # via 17 | # jinja2 18 | # werkzeug 19 | werkzeug==2.2.2 20 | # via flask 21 | -------------------------------------------------------------------------------- /apps/python/secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "super-secret" 3 | } --------------------------------------------------------------------------------