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.