diff --git a/README.md b/README.md index 55fe8f3..247bc76 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,14 @@ # ScanAPI Examples -ScanAPI examples for different API's. +ScanAPI examples for different APIs. + +| Example | Description | +|---------|-------------| +| [ci-smoke-tests](ci-smoke-tests/) | Run ScanAPI as a CI smoke test step in GitHub Actions after deployment | +| [demo-api](demo-api/) | Full-featured example using the ScanAPI Demo API | +| [github-api](github-api/) | Tests against the GitHub REST API | +| [httpbin-api](httpbin-api/) | Tests against httpbin.org | +| [httpbingo-api](httpbingo-api/) | Tests against httpbingo.org | +| [jsonplaceholder-api](jsonplaceholder-api/) | Tests against JSONPlaceholder | +| [pokeapi](pokeapi/) | Tests against the PokeAPI | diff --git a/ci-smoke-tests/.env b/ci-smoke-tests/.env new file mode 100644 index 0000000..6b9ddc3 --- /dev/null +++ b/ci-smoke-tests/.env @@ -0,0 +1 @@ +export BASE_URL="https://jsonplaceholder.typicode.com" diff --git a/ci-smoke-tests/.github/workflows/smoke-tests.yml b/ci-smoke-tests/.github/workflows/smoke-tests.yml new file mode 100644 index 0000000..03e9440 --- /dev/null +++ b/ci-smoke-tests/.github/workflows/smoke-tests.yml @@ -0,0 +1,112 @@ +# Reusable GitHub Actions workflow for running ScanAPI smoke tests +# after a deployment. Copy this file into your project's +# .github/workflows/ directory and adjust the inputs to match +# your setup. +# +# Usage: +# 1. As a standalone workflow (triggered manually or on push) +# 2. Called from another workflow after a deploy step +# +# For authenticated APIs, store your credentials as GitHub Secrets +# and pass them as environment variables (see the "with-auth" job). + +name: Smoke Tests + +on: + workflow_dispatch: + inputs: + base_url: + description: "Base URL of the deployed service" + required: true + default: "https://jsonplaceholder.typicode.com" + # Uncomment to run after every push: + # push: + # branches: [main] + # Uncomment to use as a reusable workflow called from deploy pipelines: + # workflow_call: + # inputs: + # base_url: + # required: true + # type: string + +permissions: + contents: read + +jobs: + smoke-test: + name: Run ScanAPI smoke tests + runs-on: ubuntu-latest + env: + BASE_URL: ${{ inputs.base_url || 'https://jsonplaceholder.typicode.com' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install ScanAPI + run: pip install scanapi + + - name: Run smoke tests + run: scanapi run scanapi.yaml -c scanapi.conf + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + if: always() + with: + name: scanapi-smoke-test-report + path: scanapi-report.html + + # ------------------------------------------------------- + # Example: Authenticated API + # Uncomment this job if your API requires authentication. + # Store credentials in GitHub Secrets. + # ------------------------------------------------------- + # smoke-test-with-auth: + # name: Run authenticated smoke tests + # runs-on: ubuntu-latest + # # Uncomment to run after a deploy job: + # # needs: [deploy] + # env: + # BASE_URL: ${{ inputs.base_url }} + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + # + # - name: Set up Python + # uses: actions/setup-python@v5 + # with: + # python-version: "3.12" + # + # - name: Install ScanAPI + # run: pip install scanapi + # + # - name: Obtain access token + # run: | + # response=$(curl --silent --request POST \ + # --url "https://${{ secrets.AUTH_DOMAIN }}/oauth/token" \ + # --header "Content-Type: application/x-www-form-urlencoded" \ + # --data-urlencode "client_id=${{ secrets.AUTH_CLIENT_ID }}" \ + # --data-urlencode "client_secret=${{ secrets.AUTH_CLIENT_SECRET }}" \ + # --data-urlencode "grant_type=client_credentials" \ + # --data-urlencode "audience=${{ secrets.AUTH_AUDIENCE }}") + # token=$(echo "$response" | jq -r '.access_token') + # if [ "$token" = "null" ] || [ -z "$token" ]; then + # echo "::error::Failed to obtain access token" + # exit 1 + # fi + # echo "ACCESS_TOKEN=$token" >> "$GITHUB_ENV" + # echo "::add-mask::$token" + # + # - name: Run smoke tests + # run: scanapi run scanapi.yaml -c scanapi.conf + # + # - name: Upload HTML report + # uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: scanapi-smoke-test-report + # path: scanapi-report.html diff --git a/ci-smoke-tests/README.md b/ci-smoke-tests/README.md new file mode 100644 index 0000000..631311a --- /dev/null +++ b/ci-smoke-tests/README.md @@ -0,0 +1,68 @@ +# CI Smoke Tests + +A ready-to-use example showing how to run ScanAPI as a **CI smoke test step** in GitHub Actions. Use it to verify your API is healthy after every deployment. + +This example tests the public [JSONPlaceholder](https://jsonplaceholder.typicode.com) API and demonstrates: + +- **Health checks** — verify the service is reachable +- **Read endpoint validation** — list, get, and 404 handling +- **Request chaining** — create a resource, extract its ID, and retrieve it +- **GitHub Actions workflow** — run ScanAPI after deploy and upload the HTML report as an artifact + +## Run locally + +```bash +pip install scanapi +source .env +scanapi run scanapi.yaml -c scanapi.conf +``` + +The report will be available at `scanapi-report.html`. + +## Run in CI + +Copy `.github/workflows/smoke-tests.yml` into your project. The workflow: + +1. Installs Python and ScanAPI +2. Runs the spec against your deployed service +3. Uploads the HTML report as a build artifact + +### Trigger manually + +```bash +gh workflow run smoke-tests.yml -f base_url=https://your-api.example.com +``` + +### Call from a deploy workflow + +```yaml +jobs: + deploy: + # ... your deploy steps ... + + smoke-test: + needs: [deploy] + uses: ./.github/workflows/smoke-tests.yml + with: + base_url: https://your-api.example.com +``` + +## Authenticated APIs + +The workflow includes a commented-out job showing how to obtain an OAuth2 token from GitHub Secrets before running the tests. Uncomment and adapt it to your auth provider. + +In your `scanapi.yaml`, reference the token as an environment variable: + +```yaml +headers: + Authorization: Bearer ${ACCESS_TOKEN} +``` + +And configure `scanapi.conf` to hide it from the report: + +```yaml +report: + hide_request: + headers: + - Authorization +``` diff --git a/ci-smoke-tests/scanapi.conf b/ci-smoke-tests/scanapi.conf new file mode 100644 index 0000000..2f4981c --- /dev/null +++ b/ci-smoke-tests/scanapi.conf @@ -0,0 +1,7 @@ +project_name: CI Smoke Tests +output_path: scanapi-report.html +no_report: false +report: + hide_request: + headers: + - Authorization diff --git a/ci-smoke-tests/scanapi.yaml b/ci-smoke-tests/scanapi.yaml new file mode 100644 index 0000000..a2f904b --- /dev/null +++ b/ci-smoke-tests/scanapi.yaml @@ -0,0 +1,84 @@ +endpoints: + # ------------------------------------------------------- + # Health Check + # ------------------------------------------------------- + - name: Health Check + path: ${BASE_URL} + requests: + - name: service_is_reachable + method: get + tests: + - name: returns_200 + assert: ${{ response.status_code == 200 }} + + # ------------------------------------------------------- + # Posts API — Read operations + # ------------------------------------------------------- + - name: Posts API + path: ${BASE_URL}/posts + headers: + Content-Type: application/json + requests: + - name: list_posts + method: get + params: + _limit: 2 + tests: + - name: returns_200 + assert: ${{ response.status_code == 200 }} + - name: response_is_list + assert: ${{ isinstance(response.json(), list) }} + - name: respects_limit + assert: ${{ len(response.json()) <= 2 }} + + - name: get_single_post + path: /1 + method: get + tests: + - name: returns_200 + assert: ${{ response.status_code == 200 }} + - name: has_expected_fields + assert: ${{ all(k in response.json() for k in ("id", "title", "body", "userId")) }} + + - name: get_nonexistent_post + path: /99999 + method: get + tests: + - name: returns_404 + assert: ${{ response.status_code == 404 }} + + # ------------------------------------------------------- + # Resource Lifecycle (request chaining) + # Demonstrates creating a resource and retrieving it + # using variables extracted from the previous response. + # ------------------------------------------------------- + - name: Post Lifecycle + path: ${BASE_URL}/posts + headers: + Content-Type: application/json + requests: + - name: create_post + method: post + body: + title: "ScanAPI Smoke Test" + body: "This post was created by a CI smoke test." + userId: 1 + tests: + - name: returns_201 + assert: ${{ response.status_code == 201 }} + - name: has_id + assert: ${{ "id" in response.json() }} + - name: title_matches + assert: ${{ response.json()["title"] == "ScanAPI Smoke Test" }} + vars: + created_post_id: ${{ response.json()["id"] }} + + - name: get_created_post + path: /${created_post_id} + method: get + tests: + - name: returns_200_or_404 + # JSONPlaceholder doesn't persist new posts, so the + # created ID may return 404. In a real API this would + # be a strict 200 check. We keep it to show the pattern. + assert: ${{ response.status_code in (200, 404) }}