socialdept/atp-testnet 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

socialdept/atp-testnet

Composer 安装命令:

composer require --dev socialdept/atp-testnet

包简介

Stand up a local AT Protocol testnet for integration testing in PHP

README 文档

README

Testnet Header

Stand up a local AT Protocol testnet for integration testing in PHP.


What is ATP Testnet?

ATP Testnet spins up a complete local AT Protocol network using Docker Compose for integration testing. No mocks, no fakes — real PLC directory, real PDS, real relay with firehose.

Quick Example

use SocialDept\AtpTestnet\Testnet;

// Boot the network
$testnet = Testnet::start();

// Create an account
$alice = $testnet->createAccount('alice');
echo $alice->did;    // did:plc:abc123...
echo $alice->handle; // alice.test

// Create a record
$testnet->pds()->createRecord(
    'app.bsky.feed.post',
    ['$type' => 'app.bsky.feed.post', 'text' => 'Hello from testnet!', 'createdAt' => gmdate('c')],
    $alice->accessJwt,
);

// Query the PLC directory
$doc = $testnet->plc()->getDocument($alice->did);

// Subscribe to the firehose
$testnet->requestRelayCrawl();
$frames = $testnet->relay()->consumeFirehose(timeoutMs: 3000);

// Tear down
$testnet->stop();

Installation

composer require socialdept/atp-testnet --dev

Requirements

  • PHP 8.3+
  • Docker & Docker Compose
  • ~2 GB disk for Docker images on first build

Building Docker Images

The PLC directory and relay are built from source since they don't publish official Docker images. Images are built automatically on the first Testnet::start() call, or you can pre-build them:

# Build missing images (skips if already built)
composer testnet:build

# Force rebuild all images
composer testnet:rebuild

# Or use the binary directly
vendor/bin/testnet-build
vendor/bin/testnet-build --rebuild

The first build clones the repos and compiles Go and Node — this takes a few minutes. After that, starts are instant.

The relay image is patched during build to work in Docker's internal network (SSRF bypass, hostname validation, domain ban checks, and account host matching are all disabled via the RELAY_DISABLE_SSRF environment variable).

The PLC image is patched to support disabling rate limits via the PLC_DISABLE_RATE_LIMIT environment variable (enabled by default in the testnet compose file). The production PLC enforces 10 ops/hour per DID — this patch bypasses that for local testing.

If you update the package and the build-time patches change, force a rebuild with composer testnet:rebuild or vendor/bin/testnet-build --rebuild.

Services

Service Image Default Port Purpose
PLC Directory Built from did-method-plc 7100 DID registration and PLC operations
PDS ghcr.io/bluesky-social/pds:0.4 7102 Account creation, repos, XRPC
Relay Built from indigo 7101 Firehose relay, crawls PDS

Each service also runs its own Postgres instance internally.

Usage with PHPUnit / Pest

use SocialDept\AtpTestnet\Concerns\UsesTestnet;

// PHPUnit
class MyTest extends TestCase
{
    use UsesTestnet;

    public function test_account_creation(): void
    {
        $account = $this->testnet->createAccount('bob');
        $this->assertStringStartsWith('did:plc:', $account->did);
    }
}

// Pest
uses(UsesTestnet::class);

test('can create account', function () {
    $account = $this->testnet->createAccount('bob');
    expect($account->did)->toStartWith('did:plc:');
});

The UsesTestnet trait boots the testnet once per test class and tears it down after all tests complete.

Test Isolation

The testnet accumulates state across tests (accounts, DIDs, relay subscriptions). Use the reset methods to start each test with a clean slate:

// Reset everything before each test
beforeEach(function () {
    $this->testnet->resetAll();
});

// Or reset individual services
beforeEach(function () {
    $this->testnet->resetPds();   // Clear accounts and repos only
});
Method What it clears
resetPds() All accounts, repos, sessions, invite codes (SQLite truncate)
resetPlc() All DIDs and operations (Postgres truncate)
resetRelay() All host subscriptions, account tracking, persisted data
resetAll() All of the above

Services stay running and healthy after reset — no container restarts needed.

Configuration

use SocialDept\AtpTestnet\TestnetConfig;
use SocialDept\AtpTestnet\Testnet;

$testnet = Testnet::start(new TestnetConfig(
    plcPort: 8100,
    pdsPort: 8102,
    relayPort: 8101,
    adminPassword: 'custom-admin',
));

Docker Compose Overrides

You can customize the testnet's Docker Compose configuration by publishing an override file to your project root:

cp vendor/socialdept/atp-testnet/stubs/docker-compose.testnet.yml docker-compose.testnet.yml

Edit the file to override any service configuration:

# docker-compose.testnet.yml
services:
  plc:
    environment:
      PLC_DISABLE_RATE_LIMIT: "0"  # Re-enable rate limits
  pds:
    environment:
      PDS_DEV_MODE: "0"
      PDS_HOSTNAME: custom.test

The testnet automatically detects docker-compose.testnet.yml in your working directory and merges it on top of the base configuration. Only specify values you want to change — everything else keeps its default.

No rebuild needed. Override files are applied at runtime via Docker Compose's -f merge. Changes take effect on the next Testnet::start() — no image rebuild required. Image rebuilds are only needed when the package itself updates its build-time patches (PLC rate limit bypass, relay SSRF bypass).

You can also pass an explicit path via config:

$testnet = Testnet::start(new TestnetConfig(
    composeOverride: base_path('my-custom-overrides.yml'),
));

Environment Variables

Variable Service Default Description
PLC_DISABLE_RATE_LIMIT PLC 1 Disable the 10 ops/hour rate limit per DID
RELAY_DISABLE_SSRF Relay 1 Disable SSRF protection for Docker networking
RELAY_ALLOW_INSECURE_HOSTS Relay 1 Allow HTTP (non-TLS) host connections
PDS_DEV_MODE PDS 1 Allow HTTP hostnames, disable SSRF protection
PDS_INVITE_REQUIRED PDS false Require invite codes for account creation
PDS_HOSTNAME PDS pds.test PDS hostname for handle domains

Disposable & Bring-Your-Own PDS

The shared testnet PDS uses fixed secrets and is reused across tests — fine for PLC/account/firehose assertions, but you cannot rotate or rebuild its container without breaking other tests. For tests that need a PDS they fully own (secret rotation, container rebuilds, multi-PDS migration), spawn a disposable one. Its DIDs still register in the shared testnet PLC.

use SocialDept\AtpTestnet\Data\PdsSpec;

$spawned = $testnet->spawnPds(new PdsSpec(name: 'my-pds', hostPort: 7300));

$pds = $spawned->pds();
$account = $pds->createAccount($pds->handle('alice')); // alice.my-pds.test
$doc = $testnet->plc()->getDocument($account->did);     // resolves on the shared PLC

$testnet->despawnPds($spawned); // or $testnet->stop() tears down all spawned PDSes

Only name + hostPort are required; secrets are generated when omitted so the PDS is isolated. Supply dataPath for a host bind-mount (survives container rebuilds), network to attach to $testnet->networkName(), or explicit secrets/hostname so a consumer that rebuilds the container itself can reproduce its exact configuration.

To instead run a PDS container entirely yourself against the shared testnet, point its PDS_DID_PLC_URL at $testnet->plcUrlForContainers() and (optionally) $testnet->requestRelayCrawl($yourPdsHostname) to have the relay index it.

API Reference

Testnet

// Lifecycle
Testnet::start(?TestnetConfig $config = null): Testnet
$testnet->stop(): void
$testnet->isRunning(): bool

// Accounts
$testnet->createAccount(string $handle, ?string $email = null): TestAccount
$testnet->createAccountWithSession(string $handle, ?string $email = null): TestAccount
$testnet->authenticatedClient(TestAccount $account): \GuzzleHttp\Client

// Services
$testnet->plc(): PlcService
$testnet->pds(): PdsService
$testnet->relay(): RelayService

// Relay
$testnet->requestRelayCrawl(?string $pdsHostname = null): void  // null = built-in testnet PDS

// Bring-your-own / disposable PDS (shares the testnet PLC + relay)
$testnet->plcUrlForContainers(): string   // PLC URL reachable from a container you run yourself
$testnet->networkName(): string           // compose network to attach an external container to
$testnet->spawnPds(PdsSpec $spec): SpawnedPds
$testnet->despawnPds(SpawnedPds|string $pds): void

// Reset (for test isolation)
$testnet->resetPds(): void       // Truncate all PDS accounts and repos
$testnet->resetPlc(): void       // Truncate all DIDs and operations
$testnet->resetRelay(): void     // Truncate relay subscriptions and data
$testnet->resetAll(): void       // Reset PDS + PLC + Relay

// Config
$testnet->config(): TestnetConfig
$testnet->rotationKeypair(): Secp256k1Keypair

PLC Service

// DID documents
$testnet->plc()->getDocument(string $did): array
$testnet->plc()->getDocumentData(string $did): array
$testnet->plc()->getLastOperation(string $did): array
$testnet->plc()->getOperationLog(string $did): array
$testnet->plc()->getAuditLog(string $did): array
$testnet->plc()->submitOperation(string $did, array $operation): void

// PLC operations (requires atp-cbor for signing)
$testnet->plc()->updateServiceEndpoint(string $did, string $newEndpoint, Secp256k1Keypair $signer): void
$testnet->plc()->updateHandle(string $did, string $newHandle, Secp256k1Keypair $signer): void
$testnet->plc()->updateRotationKeys(string $did, array $newRotationKeys, Secp256k1Keypair $signer): void

$testnet->plc()->isHealthy(): bool

PDS Service

// Handles — "local.{service handle domain}"
$testnet->pds()->handle(string $local): string   // built-in PDS: "local.test"
$spawned->pds()->handle(string $local): string   // spawned PDS: "local.{its hostname}"

// Accounts
$testnet->pds()->createAccount(string $handle, ?string $email = null, ?string $password = null): TestAccount
$testnet->pds()->deleteAccount(string $did, string $password, string $token): array
$testnet->pds()->deactivateAccount(string $accessJwt): array
$testnet->pds()->activateAccount(string $accessJwt): array

// Sessions
$testnet->pds()->createSession(string $identifier, string $password): array
$testnet->pds()->getSession(string $accessJwt): array
$testnet->pds()->refreshSession(string $refreshJwt): array
$testnet->pds()->deleteSession(string $refreshJwt): void

// Records
$testnet->pds()->createRecord(string $collection, array $record, string $accessJwt, ?string $repo = null): array
$testnet->pds()->getRecord(string $repo, string $collection, string $rkey): array
$testnet->pds()->deleteRecord(string $collection, string $rkey, string $accessJwt, ?string $repo = null): array
$testnet->pds()->listRecords(string $repo, string $collection, int $limit = 50): array

// Repos and blobs
$testnet->pds()->describeRepo(string $repo): array
$testnet->pds()->uploadBlob(string $data, string $mimeType, string $accessJwt): array
$testnet->pds()->getRepo(string $did): string

// Identity
$testnet->pds()->resolveHandle(string $handle): array
$testnet->pds()->updateHandle(string $handle, string $accessJwt): array

// Admin
$testnet->pds()->createInviteCode(int $useCount = 1, ?string $forAccount = null): array
$testnet->pds()->getAccountInfo(string $did): array
$testnet->pds()->adminUpdateHandle(string $did, string $handle): array
$testnet->pds()->adminUpdateEmail(string $did, string $email): array
$testnet->pds()->updateSubjectStatus(string $did, string $deactivated = 'false'): array

$testnet->pds()->describeServer(): array
$testnet->pds()->isHealthy(): bool

Relay Service

$testnet->relay()->requestCrawl(string $pdsHostname): void
$testnet->relay()->listRepos(?int $limit = null, ?string $cursor = null): array
$testnet->relay()->listHosts(?int $limit = null): array
$testnet->relay()->getRepoStatus(string $did): array
$testnet->relay()->getLatestCommit(string $did): array
$testnet->relay()->getBlob(string $did, string $cid): string
$testnet->relay()->consumeFirehose(int $timeoutMs = 3000, ?int $cursor = null): array

$testnet->relay()->isHealthy(): bool

TestAccount

$account->did;        // did:plc:abc123...
$account->handle;     // alice.test
$account->email;      // alice@test.invalid
$account->password;   // auto-generated
$account->accessJwt;  // session token
$account->refreshJwt; // refresh token

Contributing

Please see CONTRIBUTING.md for details.

License

The MIT License (MIT). Please see License File for more information.

socialdept/atp-testnet 适用场景与选型建议

socialdept/atp-testnet 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 7 次下载、GitHub Stars 达 5, 最近一次更新时间为 2026 年 04 月 11 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「testing」 「docker」 「plc」 「integration testing」 「Relay」 「atp」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 socialdept/atp-testnet 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 socialdept/atp-testnet 我们能提供哪些服务?
定制开发 / 二次开发

基于 socialdept/atp-testnet 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 7
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 5
  • 点击次数: 31
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 5
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-11