test: store contract tests, EF tests on SQLite, self-provisioning integration CI #176
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!176
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-137-tests-2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #137.
Second half of #137 (the first half, test infrastructure and low-value test removal, is #175 — already merged into
main). This half covers what #175 deferred until after #136 merged: contract base classes for every store, moving EF tests to SQLite and dropping the InMemory provider, and the integration-CI changes, plus a further pass on the handler/host migration and duplicate-fact consolidation #175 also left open.Contract base classes
Six abstract xUnit classes in
tests/ATProtoNet.Tests/TestSupport/, one per store interface, each run against every implementation by a thin derived class:SessionStoreContractTests→InMemoryAtProtoSessionStore,FileAtProtoSessionStore,EfCoreAtProtoSessionStore<T>JtiReplayStoreContractTests→InMemoryJtiReplayStore,EfCoreJtiReplayStore<T>RepoSyncStateStoreContractTests→InMemoryRepoSyncStateStore,EfCoreRepoSyncStateStore<T>SpaceAuthorityStoreContractTests→InMemorySpaceAuthorityStore,EfCoreSpaceAuthorityStore<T>SimpleSpaceStoreContractTests→InMemorySimpleSpaceStore,EfCoreSimpleSpaceStore<T>OAuthStateStoreContractTests→InMemoryOAuthStateStore,DistributedCacheOAuthStateStore(overMemoryDistributedCache)InMemorySimpleSpaceStoreandInMemoryRepoSyncStateStorehad no direct unit tests before this — only indirect coverage through other components — so the contract is their first real coverage.Implementation-specific behaviour stayed where it was, or moved into a slimmer sibling test class: file permissions and legacy-format migration (
FileAtProtoSessionStoreTests), EF schema/table names and the 0.6-row migration (EfCoreAtProtoSessionStoreTests), SQL collation and cross-restart persistence (EfCoreSpaceStoreTests, trimmed to just that plus one composition smoke test for the replay store tableSpaceDbContextshares withJtiReplayDbContext), the in-memory sweep's internal timing hooks (InMemoryJtiReplayStoreTests), and per-requester/total capacity and key hashing for the OAuth state stores. Nothing lost an assertion — where a check depended on wall-clock precision that a SQLite round-trip doesn't preserve exactly (RegisterNotifyAsyncrenewal), the contract compares with a 1-second tolerance and the EF-specific test keeps the original fake-clock version.EfCoreSpaceStoreTestssplit into three classes in the same file: the trimmed original plus newEfCoreSpaceAuthorityStoreContractTestsandEfCoreSimpleSpaceStoreContractTests, since a single class can't derive from two contract base classes.EF tests on SQLite;
Microsoft.EntityFrameworkCore.InMemorydroppedMost EF store tests were already on SQLite from earlier issues (#133). The two remaining
UseInMemoryDatabasecall sites —EfCoreAtProtoSessionStoreTestsand the two DI-registration test fixtures (JtiReplayStoreRegistrationTests,SpaceStoreRegistrationTests, which only check which store type resolves, never touch data) — now use aSqliteConnectionwithMode=Memory;Cache=Shared, held open for the test's lifetime.Microsoft.EntityFrameworkCore.InMemoryis removed fromtests/ATProtoNet.Tests.csproj;Microsoft.EntityFrameworkCore.Sqlitestays pinned to10.0.10, matchingMicrosoft.EntityFrameworkCore.Relationalinsrc/ATProtoNet.Server.EntityFrameworkCore. This removes the version-pin trap the InMemory package needed (two EF Core Abstractions assemblies loading at once,MissingMethodException) — there's nothing left to pin.Nothing was silently skipped: the
ExecuteUpdate-based space store paths (#133) already ran on SQLite before this PR and still do.Integration CI
AuthenticatedClientFixtureprovisions its own throwaway account through the admin API by default (likeSpaceNetworkFixture), instead of requiringATPROTO_TEST_HANDLE/ATPROTO_TEST_PASSWORD. Setting both env vars still overrides it.AuthenticationTests(the one suite that logged in directly againstTestConfig.Handle/Passwordrather than through the fixture) now takes its credentials from a per-classAuthenticatedClientFixture(IClassFixture, not the shared[Collection("Authenticated")], so it stays independent of the other four suites sharing that collection).pds-integrationCI job's filter widens fromPdsAdminTestsalone to also runAuthenticationTests,RepositoryTests,CustomRecordTestsandIdentityResolutionTestsagainst the reference PDS container — verified locally against the sameghcr.io/bluesky-social/pds:latestimage via podman, with onlyATPROTO_PDS_ADMIN_PASSWORDset: 30/30 passed.BlueskyFeatureTests'IntegrationRequirement.Blueskycases stay out of the filter: that container has noapp.bsky.*services, and withATPROTO_REQUIRE_INTEGRATION=1already set in that job, running them would fail rather than skip (confirmed locally — 3/3 failed withNo service configuredbefore I excluded them).RequiresTheoryAttributeremoved: it had no callers, and none of the integration suites' facts share enough shape to be worth merging into aTheory(each covers a materially different scenario against the live PDS, unlike the pure-function cases in the unit-test merges below).CONTRIBUTING.md's manual integration-test setup no longer tells contributors tocurla test account into existence by hand.Handler/host migration (continuing from #175)
Migrated onto
HttpStub/XrpcTestClientwhere it was a real simplification:Ozone/OzoneClientTests.cs,Ozone/TypedOzoneClientTests.cs— each had its own single-response or queue-basedHttpMessageHandler.Sync/SyncClientTests.cs—HttpStub'sOn(nsid, respond)lambda overload already supports the binary CAR responses this file needs (controllableContent-Type, declared length, chunked transfer), so noTestSupportchanges were needed.Spaces/SpaceClientTests.cs— built its ownXrpcClient+SpaceClient/SimpleSpaceClientdirectly, instead of going throughAtProtoClient.Space/.SimpleSpacelike every other Lexicon-domain test file.Left as-is, with reasons (this covers every file the issue named for this area):
Ozone/OzoneModerationToolingTests.cs,OzoneQueueClientTests.cs,OzoneReportClientTests.cs,OzoneToolsClientTests.cswere already migrated in #175 (they shareOzoneTestClient'sIsQuery/IsProcedureDSL) — not part of this pass's scope.Server/Authentication/AtProtoOAuthServiceHttpClientTests.cs— its two handler-using tests checkHttpClientownership (a caller-supplied client survives the service; an SDK-created one gets the configured timeout), not XRPC scripting.HttpStub/XrpcTestClientare built aroundAtProtoClient; this file testsAtProtoOAuthService's rawHttpClientsourcing, and its existing 12-line handler is already about as small as the concern needs.Spaces/SpaceCredentialMintTests.cs— itsFakeSpaceNetworkis a genuinely stateful fixture: gated/synchronized minting for concurrency tests, per-space mint counts, live DPoP-bound credential signing. AHttpStubqueue can't express "block this request until the test releases it."Spaces/SpaceCredentialProviderTests.cs— itsStaticHandlerfakes a PLC directory response (DID document resolution), not an XRPC endpoint; already a 3-line handler, andHttpSpaceClientMetadataResolverTestsalongside it fakes client-metadata/JWKS fetches the same way — neither goes throughAtProtoClient.Spaces/SpaceSyncerTests.cs— itsStubHostmutates shared state (Ops,OpsStatus,CarStatus,Car,UndeclaredLength) mid-test across two different endpoint shapes (JSON oplog, binary CAR); migrating it safely needs more verification time than this pass had.Server/Spaces/HttpSpaceClientMetadataResolverTests.cs— fakes third-party client-metadata.json/JWKS responses (including undeclared-Content-Lengthbodies) forHttpSpaceClientMetadataResolver, constructed directly over a rawHttpClient; notAtProtoClient/XRPC.Server/Spaces/SimpleSpaceManagingAppClientTests.cs,SpaceServiceAuthKeyTests.cs,SpaceWriteNotifierTests.cs— each has its own small single-responseRecordingHandlerfor a managing-app HTTP check, a namedIHttpClientFactoryclient, and a write-notification POST respectively; none go throughAtProtoClient, andSpaceWriteNotifierTestsmixes in NSubstitute mocks ofIDidResolver/ISpaceAuthorityStorethat are orthogonal to the HTTP layer. Each handler is small and self-contained; the marginal win from swapping in a differently-shapedHttpStubwasn't worth the remaining review budget in this pass.Server/Authentication/{AtProtoOAuthEndpointTests,ServiceAuthWithSpacesTests},Server/Spaces/{SimpleSpaceEndpointTests,SpaceServerEndpointTests}) — #175 deferred these pending #136's DI-registration redesign, which has since merged, so the original blocker is gone. Left for a follow-up:SpaceServerEndpointTests.csalone is 800 lines of DI-registration-heavy ASP.NET Core hosting, and consolidating four such hosts safely is a larger, separately-reviewable change than this PR's remaining scope allowed.Spaces/{SpaceCommitTests,SpaceRepoCarTests,SpaceScopeTests,SpaceTokensTests,SpaceAuthorityTests,SpaceUriTests,SpaceUnionTests,Blake3Tests,LtHashTests}.cs— pure unit tests with no HTTP handler at all; not in the issue's list.Streaming/*,Tap/TapClientTests.cs— WebSocket-based (Jetstream, Tap's event channel) or bespoke non-XRPC HTTP (Tap's admin endpoints), with their own fake connectors/sockets. Genuinely specialized;HttpStubhas no WebSocket story.Duplicate facts merged into theories
Streaming/JetstreamEventParserTests.cs: the six standaloneParse_*_ReturnsNullfacts plus the existing 4-caseParse_MalformedFrame_ReturnsNulltheory collapsed into oneParse_InvalidOrUnrecognizedFrame_ReturnsNulltheory (9 cases).Auth/OAuth/AtProtoScopesTests.cs: the pure "build a scope, assert the string" facts forRepo,Rpc,Blob,Accountand the transitional scope constants collapsed into oneTheory/MemberDatagroup each; the three "no actions is rejected" exception facts collapsed into one. Exception-shape and reflection-based facts (Repo_EmptyCollection_Throws,Rpc_BothWildcards_Throws,IdentityAction_IsObsolete, theInclude/Combine/Rpc-audience groups) stayed as they were — they check more than a single string-in/string-out relationship.The remaining ~18 groups #175's audit identified were left for a further pass; each is a smaller win than these two and none block anything else in this cycle.
Breaking changes
None. Test-project-internal only.
RequiresTheoryAttribute(unused,tests/ATProtoNet.IntegrationTestsis not packed) is removed.Verified
dotnet build -p:EnableSourceControlManagerQueries=false(Debug and Release, full solution): 0 warnings, 0 errors.dotnet test tests/ATProtoNet.Tests/ -p:EnableSourceControlManagerQueries=false: 5058 passed / 5 skipped / 5063 total, 0 failures, twice in a row (also in--configuration Release).dotnet test tests/ATProtoNet.IntegrationTests/: 85 skipped locally (no PDS configured), 0 failures.ghcr.io/bluesky-social/pds:latest, the image the CI job uses): with onlyATPROTO_PDS_URL/ATPROTO_PDS_ADMIN_PASSWORDset,PdsAdminTests+AuthenticationTests+RepositoryTests+CustomRecordTests+IdentityResolutionTests— 30/30 passed, confirmingAuthenticatedClientFixture's self-provisioning and the widened CI filter both work end to end.Deferred / flagged for the docs agent (#138)
CLAUDE.mdstill mentions a[RequiresTheory(...)]counterpart toRequiresFactAttributein the integration-test build/test section; that attribute is removed in this PR.CLAUDE.mdis out of this PR's scope (owned by #138) — flagging the stale line for that pass./home/grandiras/.claude/projects/.../memory/efcore-version-pinning.md(a Claude memory file, not part of the repo) documents the InMemory/Relational version-pin trap this PR removes entirely by dropping the InMemory package; worth archiving or updating, but it's outside the repo and outside this PR's scope.🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-27T03:58:27.323Z) — nothing to re-trigger.
@claude /review, second half of #137 (#175 was the first). This is test-only apart from ci.yml, CONTRIBUTING and one CHANGELOG bullet. Please check:
TestSupport/*ContractTests.csclasses. Does every behaviour the old implementation-specific tests asserted survive, either in the contract or in the trimmed implementation tests? That includesEfCoreSpaceStoreTests, which was split into three. The one tolerance added (1 s on theRegisterNotifyAsyncrenewal, because SQLite loses sub-millisecond precision): is it justified, and is the exact fake-clock check kept elsewhere?Mode=Memory;Cache=Shared. Can shared-cache names collide between parallel tests?AuthenticatedClientFixturenow provisions its own account through the admin API.🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
This is a large, well-executed test-only PR. I checked the parts most likely to hide bugs:
AuthenticatedClientFixtureself-provisioning: constructor args,AllowInsecureHttp,DescribeServerAsync/CreateAccountAsync/DeleteAccountAsyncsignatures all matchPdsAdminClient's actual API, and the handle-generation pattern ($"itest-{Guid.NewGuid():N}"[..16].TrimEnd('-') + domain) is copied verbatim from the already-mergedSpaceNetworkFixture.CreateActorAsync, so it's consistent with existing convention (theTrimEnd('-')is a no-op given the hex alphabet, but that's pre-existing, not new).AuthenticationTestsswitching toIClassFixture<AuthenticatedClientFixture>and usingfixture.Handle/fixture.Passwordinstead ofTestConfig.Handle/Passwordis correct and matches the stated intent of keeping this suite independent of the shared"Authenticated"collection.CanAuthenticateinTestInfrastructure.cscorrectly widens the gate toHasAccount || AdminPassword set, and the skip-reason strings are updated to match.ci.yml) uses valid vstestFullyQualifiedName~A|FullyQualifiedName~BOR syntax, consistent with the pre-existing single-term filter it replaces.[RequiresFact]bullet and the newAuthenticatedClientFixturebullet land under### Changed(not### Breaking changes), consistent with the PR description's "Breaking changes: None."mainagainstorigin/issue-137-tests-2directly to confirm all six (SessionStoreContractTests,JtiReplayStoreContractTests,RepoSyncStateStoreContractTests,SpaceAuthorityStoreContractTests,SimpleSpaceStoreContractTests,OAuthStateStoreContractTests) are actually added, and spot-checked two of them (SessionStoreContractTests,SpaceAuthorityStoreContractTests) — both are well-scoped, cross-checked against implementation (ArgumentNullException.ThrowIfNullcalls inFileAtProtoSessionStore) and free of the specific-implementation leakage the PR description says was avoided.Mode=Memory;Cache=Sharedwith a GUID-unique data source per test/class and a held-open connection, matching the pattern already used elsewhere in the suite (EfCoreJtiReplayStoreTests,SpaceNetworkFixture-adjacent code).No correctness bugs found. The scope and reasoning in the PR description are unusually well-substantiated (line-cited verification, explicit reasons for every file left unmigrated), and the code matches what it claims.
⏱ 2m 18s · 18 turns · tokens: 113.0k in / 9.4k out (+2162.0k cache-read)
📜 Full session transcript