Scenario configuration helpers

Updated Jul 14, 2026

For most tests, prefer the scenario configuration dataclasses. They provide a stable, Python-friendly way to describe GitHub users, organizations, repositories, branches, and optional issues or pull requests without relying on the simulator's internal JSON structure.

To pass a scenario into the simulator, call to_simulator_config() and return the resulting mapping from github_sim_config.

Example: single repo, single user

from simulacat import DefaultBranch, Repository, ScenarioConfig, User

scenario = ScenarioConfig(
    users=(User(login="alice"),),
    repositories=(
        Repository(
            owner="alice",
            name="rocket",
            default_branch=DefaultBranch(name="main"),
        ),
    ),
)

config = scenario.to_simulator_config()

Example: multiple repositories with public and private visibility

from simulacat import Repository, ScenarioConfig, User

scenario = ScenarioConfig(
    users=(User(login="alice"),),
    repositories=(
        Repository(owner="alice", name="public-repo"),
        Repository(owner="alice", name="private-repo", is_private=True),
    ),
)

config = scenario.to_simulator_config()

Optional issues and pull requests

Issues and pull requests are modelled in the scenario schema, but they are only serialized when requested because simulator support may vary. Pass include_unsupported=True to include them in the serialized configuration.

from simulacat import Issue, PullRequest, Repository, ScenarioConfig, User

scenario = ScenarioConfig(
    users=(User(login="alice"),),
    repositories=(Repository(owner="alice", name="rocket"),),
    issues=(Issue(owner="alice", repository="rocket", number=1, title="Bug"),),
    pull_requests=(
        PullRequest(
            owner="alice",
            repository="rocket",
            number=2,
            title="Fix",
        ),
    ),
)

config = scenario.to_simulator_config(include_unsupported=True)

Named scenario factories

simulacat provides named scenario factories for common layouts. These live in simulacat.scenario alongside the data classes:

  • single_repo_scenario(owner, name="repo", owner_is_org=False, default_branch="main")
  • monorepo_with_apps_scenario(owner, repo="monorepo", apps=("app",), owner_is_org=False)
  • empty_org_scenario(login)
  • merge_scenarios(*scenarios)

The monorepo factory represents apps as branches under apps/<name> because the simulator does not model directories. The default branch is main.

from simulacat import merge_scenarios, single_repo_scenario

base = single_repo_scenario("alice", name="alpha")
extra = single_repo_scenario("alice", name="beta")

scenario = merge_scenarios(base, extra)
config = scenario.to_simulator_config()

merge_scenarios deduplicates identical entities and raises ConfigValidationError when definitions conflict (for example, two repository definitions with the same owner and name but different visibility).

Authentication tokens

simulacat can attach an Authorization header when a scenario defines access tokens. Tokens are metadata only: the simulator does not validate token values or enforce permissions, but the github_simulator fixture uses the selected token to set the header, so clients behave as if authenticated.

Tokens are represented by AccessToken and stored on ScenarioConfig via the tokens field. When more than one token is defined, default_token selects the token value that should be applied automatically. repository_visibility accepts public, private, or all to describe intended repository visibility.

import pytest

from simulacat import AccessToken, Repository, ScenarioConfig, User

scenario = ScenarioConfig(
    users=(User(login="octocat"),),
    repositories=(Repository(owner="octocat", name="demo"),),
    tokens=(
        AccessToken(
            value="ghs_test",
            owner="octocat",
            permissions=("repo",),
            repository_visibility="private",
            repositories=("octocat/demo",),
        ),
    ),
)


@pytest.fixture
def github_sim_config():
    return scenario

When the github_simulator fixture is requested, it sets Authorization: token ghs_test on the underlying session.

Selecting a token without a ScenarioConfig requires metadata under __simulacat__ in the config mapping:

@pytest.fixture
def github_sim_config():
    return {"__simulacat__": {"auth_token": "ghs_test"}}

For a full comparison of token-based authentication with real GitHub, see Authentication mode limitations.

GitHub App installation metadata

simulacat can model GitHub App and installation metadata in scenarios. These are client-side metadata only: the simulator does not expose GitHub App endpoints or enforce installation-scoped permissions. The metadata documents test intent and integrates with the token resolution flow.

Apps are represented by GitHubApp and installations by AppInstallation. When an installation declares an access_token, it is folded into the token resolution pool alongside standalone AccessToken values. The existing default_token selection logic applies: a single token auto-selects; multiple tokens require an explicit default_token.

import pytest

from simulacat import (
    AppInstallation,
    GitHubApp,
    Repository,
    ScenarioConfig,
    User,
)

scenario = ScenarioConfig(
    users=(User(login="octocat"),),
    repositories=(Repository(owner="octocat", name="hello-world"),),
    apps=(
        GitHubApp(
            app_slug="my-bot",
            name="My Bot",
            app_id=12345,
            owner="octocat",
        ),
    ),
    app_installations=(
        AppInstallation(
            installation_id=1,
            app_slug="my-bot",
            account="octocat",
            repositories=("octocat/hello-world",),
            permissions=("contents", "pull_requests"),
            access_token="ghs_installation_token",
        ),
    ),
)


@pytest.fixture
def github_sim_config():
    return scenario

The github_app_scenario factory creates a scenario with a single app and installation:

from simulacat import github_app_scenario, merge_scenarios, single_repo_scenario

app = github_app_scenario(
    "deploy-bot",
    "Deploy Bot",
    account="octocat",
    access_token="ghs_deploy",
)

repo = single_repo_scenario("octocat", name="hello-world")
combined = merge_scenarios(repo, app)
config = combined.to_simulator_config()

For a full comparison of GitHub App authentication with real GitHub, see Authentication mode limitations.

Authentication mode limitations

The @simulacrum/github-api-simulator 0.6.x line does not validate tokens, enforce permissions, or implement rate limiting. The following tables summarize the differences between simulacat's authentication modes and real GitHub behaviour. These limitations apply across all three modes.

Cross-cutting limitations

Aspect Real GitHub simulacat
Rate limiting 60 req/h unauthenticated, 5 000 req/h authenticated No rate limiting
Secondary rate limits Concurrent request and content creation limits Not modelled
Conditional requests ETag and Last-Modified support Not implemented
OAuth applications Full OAuth 2.0 flow Explicitly out of scope
Audit logging Authentication events logged Not modelled
SAML/SSO enforcement Organization-level SSO requirements Not modelled
API versioning X-GitHub-Api-Version header Not modelled

Unauthenticated mode

When no tokens are configured, the github_simulator fixture does not set an Authorization header. The simulator responds to all implemented endpoints regardless of authentication state.

Aspect Real GitHub simulacat
Private repository access Returns 404 No visibility enforcement; all repositories visible
Endpoint restrictions Some endpoints require authentication All implemented endpoints respond
IP-based throttling Progressive throttling by IP No throttling

Token-based authentication (AccessToken)

When an AccessToken is configured, the github_simulator fixture sets Authorization: token <value> on the github3.py session. The simulator accepts the header but does not validate the token or enforce any scoping.

Aspect Real GitHub simulacat
Token validation Tokens validated server-side; invalid tokens receive 401 Any token value accepted; no validation
Permission enforcement Token scopes limit endpoint access permissions field is metadata only
Token expiration Fine-grained PATs and OAuth tokens expire No expiration
Token format validation Validates prefix format (ghp_, gho_, ghs_) No format validation
Token revocation Tokens can be revoked No revocation support
Repository visibility Token scoping controls visible repositories repository_visibility is metadata only
Repository scoping Fine-grained PATs scope to specific repositories repositories field is metadata only
Per-request token switching One token per request One token per fixture session via default_token
Authorization header format Bearer <token> or token <token> Always token <value>

GitHub App installation authentication

GitHubApp and AppInstallation models describe app metadata and per-installation access. The simulator in the 0.6.x line does not expose GitHub App endpoints. These models are client-side metadata only and are not serialized into the simulator initial state.

Aspect Real GitHub simulacat
App endpoints GET /app, GET /app/installations No GitHub App endpoints available
Installation token exchange POST /app/installations/{id}/access_tokens No token exchange; access_token is a static string
JWT authentication App authenticates with a signed JWT No JWT support
Installation-scoped permissions Per-installation permission enforcement permissions is metadata only
Installation-scoped repositories Installation limited to selected repositories repositories is metadata only
Webhook delivery Installations receive webhooks No webhook delivery
Token lifetime Installation tokens expire after one hour No expiration
Manifest flow App creation via manifest Not supported
Suspension Apps can be suspended Not modelled
Serialization App data stored on GitHub servers Models excluded from simulator config

These limitations should be revisited if a future simulator release adds authentication or GitHub App support.