Skip to main content

A PyTest plugin for mocking GCP's Secret Manager

Project description

pytest-gcpsecretmanager

A pytest plugin that provides an in-memory fake for Google Cloud Secret Manager. No Docker, no emulator, no GCP credentials required.

Both SecretManagerServiceClient (sync) and SecretManagerServiceAsyncClient (async) are transparently patched for the duration of each test.

Installation

pip install pytest-gcpsecretmanager

The plugin requires Python 3.12+ and pytest 7.0+. It does not require google-cloud-secret-manager to be installed — if the SDK is absent, fake modules are injected so that from google.cloud.secretmanager import SecretManagerServiceClient still works in your code under test.

Quick start

import pytest
from google.cloud.secretmanager import SecretManagerServiceClient

@pytest.mark.secret("my-api-key", "s3cret")
def test_reads_secret(secret_manager):
    client = SecretManagerServiceClient()
    response = client.access_secret_version(
        name="projects/test-project/secrets/my-api-key/versions/latest"
    )
    assert response.payload.data == b"s3cret"

The secret_manager fixture activates patching and returns the underlying SecretStore. The @pytest.mark.secret marker pre-populates secrets before the test runs.

Fixtures

secret_manager

Function-scoped. Activates patching and returns a SecretStore instance. Any SecretManagerServiceClient or SecretManagerServiceAsyncClient instantiated during the test will use this in-memory store.

def test_programmatic_setup(secret_manager):
    secret_manager.set_secret("db-password", "hunter2")
    secret_manager.set_secret("binary-key", b"\x00\x01\x02")

    client = SecretManagerServiceClient()
    resp = client.access_secret_version(
        name="projects/test-project/secrets/db-password/versions/latest"
    )
    assert resp.payload.data == b"hunter2"

The store also supports multiple versions:

def test_versioned_secrets(secret_manager):
    secret_manager.set_secret_sequence("rotating-key", ["v1", "v2", "v3"])

    client = SecretManagerServiceClient()
    resp = client.access_secret_version(
        name="projects/test-project/secrets/rotating-key/versions/2"
    )
    assert resp.payload.data == b"v2"

secret_manager_client

Returns a FakeSecretManagerServiceClient instance backed by the current test's store.

def test_with_client(secret_manager_client):
    secret_manager_client.create_secret(
        parent="projects/test-project", secret_id="new-secret"
    )
    secret_manager_client.add_secret_version(
        parent="projects/test-project/secrets/new-secret",
        payload={"data": b"payload"},
    )
    resp = secret_manager_client.access_secret_version(
        name="projects/test-project/secrets/new-secret/versions/1"
    )
    assert resp.payload.data == b"payload"

secret_manager_async_client

Returns a FakeSecretManagerServiceAsyncClient — same API, but all methods are async.

async def test_async_client(secret_manager_async_client):
    await secret_manager_async_client.create_secret(
        parent="projects/test-project", secret_id="async-secret"
    )
    await secret_manager_async_client.add_secret_version(
        parent="projects/test-project/secrets/async-secret",
        payload={"data": b"hello"},
    )
    resp = await secret_manager_async_client.access_secret_version(
        name="projects/test-project/secrets/async-secret/versions/1"
    )
    assert resp.payload.data == b"hello"

secret_failure_injector

Returns the active _FailureInjector for programmatic failure injection (see Failure injection below).

Markers

@pytest.mark.secret(secret_id, value, *, project=None)

Pre-populate a secret before the test runs. The default project is "test-project".

# Single version
@pytest.mark.secret("api-key", "my-key")
def test_single(secret_manager): ...

# Multiple versions (pass a list)
@pytest.mark.secret("rotating", ["v1", "v2", "v3"])
def test_versions(secret_manager): ...

# Custom project
@pytest.mark.secret("api-key", "my-key", project="my-project")
def test_custom_project(secret_manager): ...

# Stack multiple markers
@pytest.mark.secret("key-a", "value-a")
@pytest.mark.secret("key-b", "value-b")
def test_multiple(secret_manager): ...

@pytest.mark.secret_failure(method, exception, *, transient=False, count=1)

Inject a failure into a specific client method.

from pytest_gcpsecretmanager import NotFound

# Permanent failure — every call raises
@pytest.mark.secret_failure("access_secret_version", NotFound("gone"))
def test_not_found(secret_manager):
    client = SecretManagerServiceClient()
    with pytest.raises(NotFound):
        client.access_secret_version(name="projects/p/secrets/s/versions/1")

# Transient failure — fails `count` times, then succeeds
@pytest.mark.secret_failure(
    "access_secret_version", NotFound("retry me"), transient=True, count=2
)
@pytest.mark.secret("key", "val")
def test_transient(secret_manager):
    client = SecretManagerServiceClient()
    for _ in range(2):
        with pytest.raises(NotFound):
            client.access_secret_version(
                name="projects/test-project/secrets/key/versions/latest"
            )
    # Third call succeeds
    resp = client.access_secret_version(
        name="projects/test-project/secrets/key/versions/latest"
    )
    assert resp.payload.data == b"val"

Failure injection

In addition to the marker, you can inject failures programmatically via the secret_failure_injector fixture:

from pytest_gcpsecretmanager import PermissionDenied

def test_programmatic_failure(secret_manager, secret_failure_injector):
    secret_failure_injector.add_permanent_failure(
        "create_secret", PermissionDenied("nope")
    )
    client = SecretManagerServiceClient()
    with pytest.raises(PermissionDenied):
        client.create_secret(parent="projects/test-project", secret_id="x")

Available exception types: NotFound, AlreadyExists, PermissionDenied, FailedPrecondition, InvalidArgument, ResourceExhausted, DeadlineExceeded.

Supported API methods

Both sync and async clients support:

Method Description
create_secret Create a new secret
get_secret Get secret metadata
delete_secret Delete a secret and all its versions
list_secrets List secrets in a project
add_secret_version Add a new version to a secret
get_secret_version Get version metadata
access_secret_version Access the secret payload
destroy_secret_version Permanently destroy a version
disable_secret_version Disable a version
enable_secret_version Re-enable a disabled version
list_secret_versions List all versions of a secret
secret_path Static helper: build a secret resource name
secret_version_path Static helper: build a version resource name

How patching works

When the secret_manager fixture is active:

  1. If google-cloud-secret-manager is installed, the real client classes are patched via unittest.mock.patch across all known import paths (google.cloud.secretmanager, google.cloud.secretmanager_v1, etc.).
  2. If the SDK is not installed, fake modules are injected into sys.modules so that from google.cloud.secretmanager import SecretManagerServiceClient resolves to the fake client.

This means your application code doesn't need any special imports or conditional logic — the fake is transparent.

Response types

Responses use lightweight dataclasses that mirror the GCP protobuf shapes:

  • AccessSecretVersionResponse — has .name and .payload (a SecretPayload with .data bytes and .data_crc32c)
  • Secret — has .name, .replication, .create_time, .labels
  • SecretVersion — has .name, .create_time, .state

License

MIT

Project details


Download files

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

Source Distribution

pytest_gcpsecretmanager-0.2.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

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

pytest_gcpsecretmanager-0.2.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file pytest_gcpsecretmanager-0.2.0.tar.gz.

File metadata

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

File hashes

Hashes for pytest_gcpsecretmanager-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e4564db83c4f95f8a9781405999fb74766f27e1aa22ae5d0bfcf64999d25ae79
MD5 bba50f224e89f89dd8e96c05aad255c6
BLAKE2b-256 afa9e21e1249b6dce304772e60768124fbd2dd5f4f05ce1da88732ec62f091ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_gcpsecretmanager-0.2.0.tar.gz:

Publisher: publish.yml on nealepetrillo/pytest-gcpsecretmanager

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

File details

Details for the file pytest_gcpsecretmanager-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_gcpsecretmanager-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 662cbb5147bc770a067f4cf01375e9ec83b371d406a324fdeaa4a2254117ce44
MD5 885aa9d73d76a9cd17323cf0eb043880
BLAKE2b-256 b3ce01b20138329ac81cd4312578d5fc887369c44a9560d803f19cfea3e4d260

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_gcpsecretmanager-0.2.0-py3-none-any.whl:

Publisher: publish.yml on nealepetrillo/pytest-gcpsecretmanager

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