Skip to main content

HTTP server for testing environments

Project description

https://github.com/lexsca/spoof/actions/workflows/checks.yml/badge.svg https://img.shields.io/pypi/v/spoof.svg https://img.shields.io/pypi/pyversions/spoof.svg https://img.shields.io/github/license/lexsca/spoof.svg https://img.shields.io/badge/code%20style-black-000000.svg

Spoof is an HTTP server written in Python for use in test environments where mocking underlying calls isn’t an option, or where it’s desirable to have an actual HTTP server listening on a socket. Hello, functional tests!

Unlike a typical HTTP server, where specific method and path combinations are configured in advance, Spoof accepts all requests and sends either a queued response, a default response if the queue is empty, or an error response if no default response is configured. Requests can be inspected after a response is sent. This can help support testing and prototyping of HTTP clients, returning deterministic responses independent of validating requests.

Compatibility

Spoof is tested on Python 3.10 to 3.14, and has no external dependencies. It may also work on older versions of Python, but this is not supported.

Multiple Spoof HTTP servers can be run concurrently, and by default, the port number is the next available unused port. With OpenSSL installed, Spoof can also provide an SSL/TLS HTTP server. IPv6 is fully supported.

Quickstart

Queue multiple responses, verify content, and request paths:

import requests
import spoof

with spoof.HTTPServer() as httpd:
    responses = [
        [200, [("Content-Type", "application/json")], '{"id": 1111}'],
        [200, [("Content-Type", "application/json")], '{"id": 2222}'],
    ]
    httpd.queueResponse(*responses)
    httpd.defaultResponse = [404, [], "Not found"]

    assert requests.get(httpd.url + "/path").json() == {"id": 1111}
    assert requests.get(httpd.url + "/alt/path").json() == {"id": 2222}
    assert requests.get(httpd.url + "/oops").status_code == 404
    assert [r.path for r in httpd.requests] == ["/path", "/alt/path", "/oops"]

Set a callback as the default response:

import requests
import spoof

with spoof.HTTPServer() as httpd:
    httpd.defaultResponse = lambda request: [200, [], request.path]

    assert requests.get(httpd.url + "/alt").content == b"/alt"

Test queued response with SSL:

import requests
import spoof

with spoof.SelfSignedSSLContext() as selfSigned:
    with spoof.HTTPServer(sslContext=selfSigned.sslContext) as httpd:
        httpd.queueResponse([200, [], "No self-signed cert warning!"])
        response = requests.get(httpd.url + "/path",
                                verify=selfSigned.certFile)

        assert httpd.requests[-1].method == "GET"
        assert httpd.requests[-1].path == "/path"
        assert response.content == b"No self-signed cert warning!"

Proxy Mode

Spoof also supports proxying HTTP requests by setting the upstream attribute to another Spoof instance:

import requests
import spoof

with spoof.SelfSignedSSLContext(commonName="example.spoof") as ssl:
    with spoof.HTTPServer(sslContext=ssl.sslContext) as proxy:
        with spoof.HTTPServer(sslContext=ssl.sslContext) as upstream:
            proxy.upstream = upstream
            proxy.defaultResponse = [200, [("X-Spoof-Proxy", "True")], ""]
            upstream.defaultResponse = [200, [], "I'm here!"]
            response = requests.get(
                "https://example.spoof/ayt",
                proxies={"https": proxy.url},
                verify=ssl.certFile
            )
            assert proxy.requests[0].method == "CONNECT"
            assert proxy.requests[0].path == "example.spoof:443"
            assert upstream.requests[0].method == "GET"
            assert upstream.requests[0].path == "/ayt"
            assert response.content == b"I'm here!"

SSL Warnings

Some libraries like Requests will complain loudly or refuse to connect to HTTP servers with a self-signed SSL certificate. The preferred way to handle this is to use the verify property in requests.Session to trust the certificate:

import requests
import spoof

cert, key = spoof.SSLContext.createSelfSignedCert()
sslContext = spoof.SSLContext.fromCertChain(cert, key)
httpd = spoof.HTTPServer(sslContext=sslContext)
httpd.queueResponse([200, [], "OK"])
httpd.start()

# trust self-signed certificate
session = requests.Session()
session.verify = cert

response = session.get(httpd.url + "/uri/path")
print(response.status_code, response.content)
httpd.stop()

If verifying the certificate is not an option, another way to work around this is to monkeypatch the requests library in the testing code. For example:

import requests

certVerify = requests.adapters.HTTPAdapter.cert_verify
def certNoVerify(self, conn, url, verify, cert):
    return certVerify(self, conn, url, False, cert)
requests.adapters.HTTPAdapter.cert_verify = certNoVerify
requests.packages.urllib3.disable_warnings()

Another common case is libraries that leverage ssl directly. One way to work around this is to globally set the default context to unverified. For example:

import ssl

try:
    createUnverifiedHttpsContext = ssl._create_unverified_context
except AttributeError:
    # ignore if ssl context not verified by default
    pass
else:
    ssl._create_default_https_context = createUnverifiedHttpsContext

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spoof-2.0.1.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spoof-2.0.1-py3-none-any.whl (10.8 kB view details)

Uploaded Python 3

File details

Details for the file spoof-2.0.1.tar.gz.

File metadata

  • Download URL: spoof-2.0.1.tar.gz
  • Upload date:
  • Size: 16.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for spoof-2.0.1.tar.gz
Algorithm Hash digest
SHA256 061ff6f022c4186137c7f5a2a41730c8cd18e0a0d5ba5e8158ffd83321aec033
MD5 f76fe621f3aa0f8bb0a8a31e5e130a7f
BLAKE2b-256 21ef2115f6ac88266776b2c91722293092aac5483f0cf0117c9fac9cd625cc15

See more details on using hashes here.

Provenance

The following attestation bundles were made for spoof-2.0.1.tar.gz:

Publisher: release.yml on lexsca/spoof

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file spoof-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: spoof-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 10.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for spoof-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0909ef185040b41208024ed2df74b7ecef9b492e52cf0e5a018b55ec15ce4bfc
MD5 708962b94372ee19921d085de4591d7d
BLAKE2b-256 8c67c8918f394f245e747a2e5f4c83a4a2ffada1094190578c63541b83ba6ab8

See more details on using hashes here.

Provenance

The following attestation bundles were made for spoof-2.0.1-py3-none-any.whl:

Publisher: release.yml on lexsca/spoof

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page