Metadata-Version: 2.4
Name: swaggerforge
Version: 0.4.0
Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
Author: Viktor Pylypenko
License-Expression: MIT
Keywords: openapi,swagger,pytest,test generation,api testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: prance>=23.6.21.0
Requires-Dist: openapi-spec-validator>=0.7
Requires-Dist: jinja2>=3.0
Requires-Dist: requests>=2.28
Requires-Dist: jsonschema>=4.0
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=9.0; extra == "dev"
Requires-Dist: flake8>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=7.0; extra == "dev"
Requires-Dist: tox>=4.0; extra == "dev"
Provides-Extra: oauth2
Requires-Dist: requests-oauth2client>=1.6; extra == "oauth2"
Dynamic: license-file

# SwaggerForge

Automatic pytest test generation from OpenAPI (Swagger) specifications.

SwaggerForge is a Python library and command-line tool that reads an OpenAPI
specification and generates ready-to-run pytest test files - one per resource
covering positive, negative, boundary, and boolean scenarios grounded in
established test-design techniques.

## Features

- Reads OpenAPI 3.x specifications in JSON or YAML
- Resolves `$ref` references automatically
- Generates one pytest file per resource tag
- Produces six scenario types per endpoint where applicable:
  - **Positive** >>> valid request, expects a 2xx response and validates the
    response schema
  - **Missing required field** >>> omits a required field, expects 400
  - **Wrong data type** >>> sends a mistyped field, expects 400/422
  - **Nonexistent resource** >>> requests a resource that was created and then
    deleted, so its absence is guaranteed (or, when the API offers no delete,
    an unlikely identifier); expects 404
  - **Boundary values** >>> tests values at and just beyond declared
    numeric/length limits (Boundary Value Analysis)
  - **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
- Stateful tests: operations on an existing resource run against one the test
  creates itself and removes afterwards, instead of a guessed identifier
- Valid request data built from the specification's own examples, defaults,
  enums and constraints
- Deterministic output: the same specification always produces identical tests
- Generated files use pytest fixtures and run with no manual edits
- Optional `swaggerforge.toml` config file for project-level defaults
- Comprehensive authentication: bearer, basic, API key (header/query/cookie),
  multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
  extra), with environment-variable substitution for secrets

## Requirements

- Python 3.10 or newer

## Installation

```bash
pip install swaggerforge
```

## Usage

Generate tests from a specification, pointing at the base URL of the API
under test:

```bash
swaggerforge generate --spec swagger.json --url http://localhost:8080
```

This reads `swagger.json`, writes one `test_<resource>.py` file per resource
tag into the output directory (default: `tests_generated/`), and the files can
be run immediately:

```bash
pytest tests_generated
```

### Options

| Option     | Description                                        | Default                           |
|------------|----------------------------------------------------|-----------------------------------|
| `--spec`   | Path to the OpenAPI specification (JSON or YAML)   | *(required)*                      |
| `--url`    | Base URL of the API under test                     | *(required unless in config)*     |
| `--output` | Directory for the generated test files             | `tests_generated`                 |
| `--config` | Path to a configuration file                       | `./swaggerforge.toml` if present  |

## Configuration

Options that stay the same across runs can be kept in a `swaggerforge.toml`
file instead of being passed on the command line. The file is picked up
automatically from the directory where the tool is run, or an explicit path
can be given with `--config`.

```toml
# swaggerforge.toml
base_url = "http://localhost:8080"
output_dir = "tests_generated"
timeout = 30
```

| Key          | Type    | Effect                                                        |
|--------------|---------|---------------------------------------------------------------|
| `base_url`   | string  | Base URL of the API; makes `--url` optional                   |
| `output_dir` | string  | Directory for generated files                                 |
| `timeout`    | integer | Embeds `timeout=<n>` into every generated HTTP request call   |

Values given on the command line always take precedence over the config file.
Without a `timeout`, generated tests place no time limit on requests -
setting one makes test runs fail fast when the API is unreachable.

## Authentication

When a specification declares security requirements, SwaggerForge configures
the generated tests to authenticate. Credentials are supplied through the
config file; the specification decides *which* scheme and *where* each value
goes, and the config supplies the secret values.

```toml
# swaggerforge.toml
[auth.bearer]
token = "${API_TOKEN}"

[auth.api_key]
value = "${API_KEY}"

[auth.basic]
username = "${API_USER}"
password = "${API_PASSWORD}"

[auth.oauth2]
client_id = "${OAUTH_CLIENT_ID}"
client_secret = "${OAUTH_CLIENT_SECRET}"
scope = "read write"
```

Generated tests build a session-scoped `requests.Session` that carries the
resolved authentication, so every request is authenticated without repeating
credentials per call.

### Supported schemes

| Config section  | OpenAPI scheme                              | How it is applied                                   |
|-----------------|---------------------------------------------|-----------------------------------------------------|
| `[auth.bearer]` | `http` bearer                               | `Authorization: Bearer <token>` header              |
| `[auth.api_key]`| `apiKey` in header, query, or cookie        | the name and location declared by the specification |
| `[auth.basic]`  | `http` basic                                | `Authorization: Basic <base64>` header              |
| `[auth.oauth2]` | `oauth2` client credentials, `openIdConnect`| a token fetched at runtime (see below)              |

For an API key, both the **name** and the **location** (header, query, or
cookie) come from the specification; the config supplies only the secret
`value`.

### Multiple schemes

OpenAPI can require several schemes at once (AND) or offer alternatives (OR).
SwaggerForge honours both: it uses the first alternative whose credentials are
fully configured, combining every scheme in an AND group. When endpoints in one
file need different authentication, the differing ones override the session per
request.

### OAuth2 and OpenID Connect

OAuth2 and OIDC support is an optional extra, installed with:

```bash
pip install swaggerforge[oauth2]
```

For the **client credentials** flow, generated tests fetch a fresh access
token from the specification's `tokenUrl` at runtime (and refresh it on
expiry), so no token is ever embedded at generation time. **OpenID Connect**
schemes work the same way, discovering the token endpoint at runtime from the
specification's `openIdConnectUrl`. Both need `client_id` and `client_secret`
in `[auth.oauth2]`.

Interactive flows (OAuth2 **authorization code**) require a human to log in
through a browser and so cannot be fully automated. For these, SwaggerForge
generates a clearly marked placeholder in the session fixture where you paste a
token obtained manually.

### Environment variables and missing credentials

Any auth value may reference an environment variable with `${VAR}` syntax, so
secrets stay out of the config file; a referenced variable that is not set is
an error. If a specification requires a scheme whose credentials are not
configured, SwaggerForge prints a warning naming the scheme and generates the
tests without that authentication rather than failing.

## Stateful tests

Endpoints that act on an existing resource - `GET`, `PUT`, `PATCH` or
`DELETE` on `/pets/{petId}` - need a resource that really exists. SwaggerForge
detects the operation that creates it and generates fixtures that create the
resource before a test and remove it afterwards.

A create operation is recognised by REST convention: a `POST` on a collection
path (`/pets`) produces resources addressed by that path plus one parameter
(`/pets/{petId}`). The new resource's identifier is taken from the response
field named like the path parameter, or else from `id`. When the
specification declares explicit OpenAPI `links`, they take priority.

For each such resource, generated files contain two fixtures:

- `created_<resource>` creates the resource, yields its identifier (read from
  the response body, or from the `Location` header) and deletes it after the
  test
- `deleted_<resource>` creates the resource and deletes it immediately, giving
  the nonexistent-resource test an identifier that is guaranteed not to exist

```python
@pytest.fixture
def created_pet(base_url, api_session):
    response = api_session.post(f"{base_url}/pet", json={'id': uuid.uuid4().int >> 80, 'name': 'doggie', 'photoUrls': ['string']})
    assert response.status_code in [200], (
        f"Creating pet failed: "
        f"{response.status_code} {response.text}"
    )
    resource_id = _extract_id(response, 'id')
    yield resource_id
    try:
        api_session.delete(f"{base_url}/pet/{resource_id}")
    except requests.RequestException:
        pass


def test_get_pet_by_id_success(base_url, api_session, created_pet):
    """GET /pet/{petId} - positive scenario"""
    response = api_session.get(f"{base_url}/pet/{created_pet}")
    assert response.status_code in [200]
```

*(Abridged from a file generated for the Swagger Petstore specification.)*

Every scenario on such an endpoint uses the created resource - negative
scenarios included, so a 404 for a missing resource cannot hide the 400 a
validation test expects. A top-level `id` in the create request is sent as a
fresh unique value on every run, so tests never share or collide on one
resource; whether the server keeps that value or assigns its own, the fixture
reads the actual identifier from the response. The generated files themselves
stay identical for the same specification - only the runtime data varies.

Endpoints without a recognisable create operation are generated exactly as
before, with example identifiers.

## Request data

Request bodies and parameters are built deterministically from the
specification, without random data. For each value, the first available
source is used: `const`, then `example` (or the first of `examples`), then
`default`, then the first `enum` member, and finally a placeholder matching
the declared type and format (for example `user@example.com` for an `email`).
Placeholder strings are fitted to `minLength` and `maxLength`. Properties
marked `readOnly` are left out of requests, as OpenAPI prescribes.

## How it works

SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
into an internal model (with `$ref`s resolved), turned into test scenarios
based on test-design techniques - with dependencies between operations
inferred, so stateful scenarios can share created resources - rendered into
pytest code via templates, and written to per-resource files.

## Limitations

- Targets OpenAPI 3.x with JSON request bodies
- Authentication covers bearer, basic, API key (header/query/cookie), OAuth2
  client credentials, and OpenID Connect; interactive OAuth2 flows
  (authorization code) generate a placeholder for a manually obtained token,
  as they require a browser-based login that cannot be automated
- Boundary tests require the specification to declare numeric/length constraints
- Stateful tests cover single-level resources (a collection and its items);
  nested chains such as `/owners/{ownerId}/pets/{petId}` keep example
  identifiers, and only `links` passing a top-level response field by
  `operationId` are used
- APIs that soft-delete (a deleted resource still answers 200) fail the
  nonexistent-resource test - a reflection of the API's semantics
- Resource fixtures use the file's session authentication; a create operation
  that needs different credentials than the file's default is not overridden
- Only a top-level field named `id` is treated as a server-side surrogate key;
  a differently named one is sent with its example value
- An identifier read from a `Location` header arrives as a string
- The create operation's own positive test does not remove the resource it
  creates

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file.
