test: shared test infrastructure, network-free unit tests, fewer low-value tests #175
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!175
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-137-tests"
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?
Part of #137 (first half: test infrastructure, low-value tests, core coverage).
What changed
Shared scripted
HttpMessageHandlerstub andXrpcTestClienttests/ATProtoNet.Tests/TestSupport/HttpStub.cs: one scriptedHttpMessageHandler. A route is keyed by its XRPC NSID (for/xrpc/{nsid}) or by the full path otherwise;On(route, ...)queues a response (the last one queued repeats),To(route)returns the requests a route saw, and an unscripted request throws instead of returning a default — so a test that forgot to script something fails loudly instead of silently passing against whatever the fallback happened to return. Added along the way, once real files needed them:Fallback(...)— answers any request with nothing scripted, for tests that assert on a cross-cutting concern (headers, retries, timeouts, service-URL switching) across many endpoints without caring which one a given call names.Delay— an artificial per-response delay, for client-side timeout/cancellation tests.ClearRequests()— forgets requests recorded so far (a test that signs in before the calls it actually wants to assert on).ContentType/ContentLengthbefore buffering its body, sinceHttpContent.Headers.ContentLengthcan turn fromnull(a non-seekable stream sent chunked) into the buffered byte count once read — this broke a chunked-upload assertion during migration and is fixed at the stub level.tests/ATProtoNet.Tests/TestSupport/XrpcTestClient.cs: a small fixture pairing anHttpStubwith a preconfiguredAtProtoClient.ScriptedXrpcHandler(used by 10 files) intoHttpStuband migrated its consumers.Migrated to
HttpStub(35 files total, replacing their ownMockHttpMessageHandler/CapturingHandler/RecordingHandler/etc.): the 10 formerScriptedXrpcHandlerconsumers underLexicon/App/Bsky/**, plus 25 more —AtProtoClientConstructionTests,RecordCollectionErrorTests,Admin/PdsAdminClientTests,Admin/PdsAdminClientAccountAuthTests,Http/XrpcClientProxyTests,Http/XrpcClientRepoRevTests,Http/XrpcClientRateLimitTests,Http/XrpcClientAdminAuthTests,Http/XrpcClientLabelerHeaderTests,Http/EmptyResponseBodyTests,Http/CustomXrpcTests,Http/XrpcTransportTests,Http/PaginationTests,Lexicon/WireFidelityTests,Lexicon/App/Bsky/TypedBskyClientTests,Lexicon/App/Bsky/Labeler/LabelerClientTests,Lexicon/Com/AtProto/ComAtProtoGapTests,Lexicon/Com/AtProto/IdentityClientTests,Lexicon/Site/Standard/StandardSiteClientTests,Lexicon/Com/AtProto/TypedRepoClientTests, andChat/Bsky/{Actor,Notification,Moderation,Group,Convo}ClientTests(5 files). That accounts for the issue's "27 hand-rolled handlers" estimate and then some; the remainder below were deliberately not touched.Deliberately left alone (already-shared, non-duplicated fixtures specialized enough that folding them into a generic stub would lose real capability, not gain consistency):
Auth/SessionTestKit.cs(StubServer) — serves 5 Auth/OAuth test files; understands OAuth metadata documents, DPoP proof claims and form-encoded token bodies, none of whichHttpStubneeds elsewhere.Identity/IdentityTestSupport.cs(ScriptedHandler+ a real loopbackTcpListener-based server) — serves 3 Identity files; the loopback server exists specifically to exercise the real socket handler (truncated bodies, broken encodings) safely.Ozone/OzoneTestClient.cs— serves 4 Ozone files with anIsQuery/IsProcedureassertion DSL.BlueskyHelpersTests.cs(FakePds) andAuth/OAuth/OAuthCallbackHandleVerificationTests.cs(FlowStub) — stateful, single-file fakes (a small in-memory profile store with CAS-retry behaviour; a whole simulated OAuth deployment with revocation tracking) that don't map onto a per-route scripted queue without losing behaviour.Not migrated, left for a follow-up (time-boxed; none of these are hand-rolled duplicates of the pattern the shared stub targets, and several sit next to the DI surface #136 is actively redesigning):
Ozone/{OzoneClientTests,TypedOzoneClientTests},Server/Authentication/AtProtoOAuthServiceHttpClientTests, theSpaces/**andServer/Spaces/**handler-based files (SpaceClientTests,SpaceCredentialMintTests,SpaceCredentialProviderTests,SpaceSyncerTests,HttpSpaceClientMetadataResolverTests,SimpleSpaceManagingAppClientTests,SpaceServerEndpointTests,SpaceServiceAuthKeyTests,SpaceWriteNotifierTests),Streaming/{JetstreamArchiveClientTests,JetstreamArchiveClientPublicEndpointTests,JetstreamReplayConsumerTests},Sync/SyncClientTests(binary CAR fixtures),Tap/TapClientTests.One
XrpcTestHostServer/XrpcEndpointTests.csalready had a well-builtXrpcTestHost+CapturingLoggerProvider, reused by four other files (ServiceAuthSchemeTests,QueryLabelsEndpointTests,XrpcQueryBindingTests,XrpcRouteGroupTests) only because they sat in a descendant namespace — an implicit, fragile sharing mechanism. Moved both intotests/ATProtoNet.Tests/TestSupport/, alongsideHttpStub, so they're found by an explicitusinglike every other shared fixture; added amapXrpcEndpointsflag so a host that maps only its own endpoints (noIXrpcQuery/IXrpcProcedureregistered) can opt out of the unconditionalMapXrpcEndpoints(), which throws without one.The other TestServer-based hosts (
Server/Authentication/{AtProtoOAuthEndpointTests,ServiceAuthWithSpacesTests},Server/Spaces/{SimpleSpaceEndpointTests,SpaceServerEndpointTests}) were left as their own bespokeHostBuilder/UseTestServer()setups: each wires upAddAtProtoServer/AddAtProtoSpaces/OAuth DI registration that #136 is actively redesigning, so consolidating them now risks conflicting with, or being redone by, that work.Server/TapWebhookTests.csusesWebApplication.CreateSlimBuilder()deliberately (a minimal host for pure endpoint-routing tests) rather than the full genericHost; left as-is.One enum-driven
[RequiresFact]/[RequiresTheory]RequiresPdsFactAttribute,RequiresBlueskyFactAttribute,RequiresPdsAdminFactAttribute,RequiresJetstreamFactAttribute,RequiresJetstreamArchiveFactAttributeandRequiresSpacesFactAttributeare replaced by oneIntegrationRequirementenum and[RequiresFact(IntegrationRequirement.X)]/[RequiresTheory(IntegrationRequirement.X)], intests/ATProtoNet.IntegrationTests/TestInfrastructure.cs.RequiresPdsFactandRequiresBlueskyFactused to ignoreATPROTO_REQUIRE_INTEGRATIONand always skip when their prerequisite was missing; the unified attribute honours it like the others (the attribute lets the test run instead of skipping, and the test fails on its own for lacking what it needs — verified live: withATPROTO_REQUIRE_INTEGRATION=1and no PDS configured,AuthenticationTestsnow fails with a real connection/argument error instead of reporting 7 skips).RequiresRedisFactAttributeis untouched (#136 removes the Redis store). All ~90 attribute usages acrosstests/ATProtoNet.IntegrationTests/*.cswere updated;CONTRIBUTING.md,docs/testing-spaces.mdandCLAUDE.mdupdated to match.Unit tests must not touch the network
Auth/OAuth/SecurityTests.cs's SSRF tests (ResolveFromIdentifier_RefusedDidWeb_IsInvalidDidWithoutARequestetc.) built theirIdentityResolverviaIdentityResolver.CreateDefault(...), which owns its own realHttpClients for DID and handle resolution — entirely separate from the scripted handler passed toAuthorizationServerDiscovery. If the SSRF pre-check under test ever regressed, a refused identifier (169.254.169.254,10.0.0.1, ...) could reach a real socket instead of the test's stub, from inside a unit test run. Rewired the DID/handle resolvers through the same scripted handler usingPlcClient/DidWebResolver/HandleResolver's existingHttpClient-accepting constructors — nosrc/change needed, the seam already existed. Also replaced two barenew HttpClient()instances (in tests that only validate a URI and never send anything) with a handler that throws if one ever does.Audited the rest of
tests/ATProtoNet.Testsfor real DNS/socket/TCP usage (Dns.*,TcpClient,Socket, bareHttpClient()): the only other loopback socket usage (Identity/IdentityFetchPolicyTests.cs) already uses an ownedTcpListeneronIPAddress.Loopback, which is the sanctioned pattern for the one case (the SSRF connect-callback policy) that has to exercise a real socket. NoAtProtoClientApplyOAuthTests.csfile exists in the current tree — the issue's brief evidence predates a rename or removal; nothing further to change there.Removed tests that test nothing
An audit against the existing blanket coverage (
Identity/IdentifierContractTests, the Lexicon drift/JSON-name tests, and the pinned interop fixtures for DAG-CBOR/CID/crypto) found 30 test methods (one whole file) that assert nothing beyond what that coverage, or a sibling test in the same file, already pins. None carry a bug/issue/regression comment and none assert a security property; every removal was verified against the specific test claimed to already cover it before deleting.OAuthModelsTests.{OAuthException_HasErrorCode,OAuthException_WithInnerException,JsonWebKey_Properties,JsonWebKeySet_Properties},RecordCollectionTests.{AtProtoRecord_Type_ReturnsCorrectNsid,AtProtoRecord_CanSetCustomCreatedAt}— property echoes with no serialization or behaviour exercised.RecordKeyTests.Self_IsValidSingleton—RecordKey.Selfis already asserted (== (RecordKey)"self") byIdentifierContractTests.ExplicitConversionsFromString_ParseOrThrow.ChatClientsTests.cs(whole file — sub-clientNotNullonly),AtProtoClientConstructionTests.Constructor_SubClients_AreAccessible,StandardSiteClientTests.AtProtoClient_Site_IsAvailable,OzoneClientTests.Ozone_Property_Exists,DynamicPdsTests.{ServiceUrl_DefaultValue,Session_NullByDefault},RecordCollectionTests.{AtProtoRecord_DifferentTypes_HaveDifferentNsids,RecordPage_CanContainMultipleRecords}— allAssert.NotNull/trivial-inequality/count checks that every real test of the same sub-client or type already exercises implicitly.IdentitySerializationTests.{Did,Nsid,AtUri,Tid,RecordKey}_RoundTrips(each type already has a*_Serializes_AsStringpinning the exact wire string — keptCid_RoundTrips, which additionally pins one);DagCborRoundtripTests.{Roundtrip_TypeField_Preserved,EncodeThenComputeCid_IsConsistent}(verbatim duplicate ofDagCborEncoderTests.Encode_TypeDiscriminator_Preserved; determinism covered with pinned bytes+CID byDagCborInteropTests.Encode_DataModelFixture_MatchesReferenceBytesAndCid);MstNodeDataTests.ToBytes_IsDeterministic(pinned by the reference vectors andToBytes_AbsentLinks_AreWrittenAsNull);CidComputationTests.{ComputeForDagCbor_ProducesValidCid,ComputeForDagCbor_IsDeterministic}(pinned byCidTests.ComputeForDagCbor_ReturnsParsedCid);DidTests.{ExplicitCast_FromString_Works,CompareTo_Orders_Correctly},HandleTests.ExplicitCast_FromString_Works,TidTests.CompareTo_OrdersCorrectly,AtUriTests.CompareTo_IsOrdinal— all duplicate the sharedIdentifierContractTests.AssertContract<T>helper, which every identifier type already runs through (CompareToordering both directions,CompareTo(null), and the explicit string cast).Unit test count goes from 4993 (after the coverage addition above) to 4963, all still green. A "merge duplicate facts into one theory" pass (about 20 groups the audit identified, e.g.
JetstreamEventParserTests's severalParse_*_ReturnsNullfacts,AtProtoScopesTests's constant/scope-building facts) was scoped out of this PR to keep the diff reviewable; noted for a follow-up.Coverage for core paths
AtProtoClient.LoginAsync/ResumeSessionAsync/LogoutAsync: already thoroughly covered byAuth/PasswordSessionLifecycleTests.cs(sign-in, resume with refresh, reactive/proactive refresh, sign-out, disposal) — checked first per the issue; nothing to add.RecordCollection<T>CRUD:Create/Get/Find/Put/List/Exists/Enumeratewere already covered (RecordCollectionErrorTests.cs,Http/PaginationTests.cs);DeleteAsynchad no test through the typed wrapper (only viaRepoClient.DeleteRecordAsyncdirectly) — addedDeleteAsync_SendsTheRepoCollectionAndKeyandDeleteAsync_WithASwapRecord_SendsIt.QueryAsync/ProcedureAsync,IXrpcTransport): already covered byHttp/CustomXrpcTests.cs(typed and anonymous-object parameters, void/parameterless overloads,IXrpcTransportused by a third-party-style sub-client, download/upload, NSubstitute fake for a sub-client's own tests) — migrated toHttpStub, no gap found.Breaking changes
None to the published SDK. Test-project-internal rename:
RequiresPdsFactAttribute/RequiresBlueskyFactAttribute/RequiresPdsAdminFactAttribute/RequiresJetstreamFactAttribute/RequiresJetstreamArchiveFactAttribute/RequiresSpacesFactAttribute→[RequiresFact(IntegrationRequirement.X)](tests/ATProtoNet.IntegrationTestsis not packed).RequiresRedisFactAttributeunchanged.Verified
dotnet build -p:EnableSourceControlManagerQueries=false(full solution, Debug and Release): 0 warnings, 0 errors.dotnet test tests/ATProtoNet.Tests/ -p:EnableSourceControlManagerQueries=false: before (start of this branch) 4991 passed / 5 skipped / 4996 total; after theDeleteAsynccoverage addition, 4993 passed; after removing the 30 low-value tests, 4963 passed / 5 skipped / 4968 total, 0 failures (also verified in--configuration Release).dotnet test tests/ATProtoNet.IntegrationTests/: 90 skipped locally (unchanged); withATPROTO_REQUIRE_INTEGRATION=1and no PDS configured, the PDS-gated tests fail instead of skipping (spot-checkedAuthenticationTests, 7/7 now fail with a real connection/argument error).Deferred to the second half (after #136 merges), per the issue's own split
Token-store/space-store contract base classes; EF tests on SQLite (dropping
Microsoft.EntityFrameworkCore.InMemory); the integration-CI changes (AuthenticatedClientFixtureprovisioning, widening the CI filter). Also deferred, noted above: the remaining hand-rolled handlers in the Spaces/Streaming/Sync/Tap areas, and the four Space/OAuth TestServer hosts that overlap #136's DI redesign.🤖 Generated with Claude Code
Add tests/ATProtoNet.Tests/TestSupport/{HttpStub,XrpcTestClient}.cs: a scripted HttpMessageHandler that keys routes by XRPC NSID or path, records every request, and throws on an unscripted one instead of silently answering. Fold the Bsky-only ScriptedXrpcHandler into it and migrate its ten consumers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>🤖 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:07:10.725Z) — nothing to re-trigger.
@claude /review — test-only apart from the CONTRIBUTING, CLAUDE.md and testing-docs updates for the new attribute. Please check:
(1) Removed tests. For each one listed in the description, confirm that what it covered is still covered, and that none was a regression or security test. Use
git diff origin/main...HEAD --diff-filter=Dplus the removed methods in modified files.(2) The migration to
TestSupport/HttpStub.cs/XrpcTestClient.cs. Does any assertion get weaker? That includes request-body or header assertions dropped, and a stub that now accepts requests the old handler would have rejected.(3) The
SecurityTestsSSRF rewiring. Does it still exercise the real guard (IdentityFetch/the connect-callback policy) rather than a stub that bypasses it?(4)
RequiresFact/IntegrationRequirement. Check thatATPROTO_REQUIRE_INTEGRATIONturns a missing prerequisite into a failure for every requirement.🤖 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 but mechanical test-infrastructure refactor, and it holds up well under verification. I spot-checked the key claims against the actual branch content (not just the diff):
HttpStubimplementation is solid: thread-safe under one lock, throws loudly on an unscripted route (no silent-default footgun), correctly readsContentType/ContentLengthbefore buffering the body, and the repeat-last-queued-response semantics (queue.Count > 1 ? Dequeue() : Peek()) match the documented behavior and are exercised correctly by the migrated concurrent-call tests (e.g.ConcurrentAdminCalls_SignInOnlyOnce).IntegrationRequirementenum consolidation inTestInfrastructure.cspreserves the exact prior skip-reason text and gating logic per requirement, and correctly generalizesATPROTO_REQUIRE_INTEGRATIONto also cover Pds/Bluesky (which previously ignored it) — matches the PR's stated behavior change.SecurityTests.csSSRF rewiring: theCachingDidResolver/DidResolver/PlcClient/DidWebResolver/HandleResolver/IdentityResolverconstructor overloads it now uses all exist with matching signatures insrc/ATProtoNet/Identity/*— the PR's claim that "no src/ change needed, the seam already existed" checks out.git grepthat no leftover references to the oldRequiresPdsFactAttribute/RequiresBlueskyFactAttribute/etc. remain in code (only historical CHANGELOG entries for already-released versions, which correctly stay as-is), and confirmedtests/ATProtoNet.Tests/Chat/ChatClientsTests.cswas actually deleted as claimed.One very minor nit noted inline:
RequiresTheoryAttributeis added with no current callers. Not a blocker — it's cheap, documented as the Fact counterpart, and plausibly there to complete the pairing, but it is currently dead code by strict repo convention ("don't add features beyond what the task requires").Overall this looks like careful, well-verified work — approving.
⏱ 2m 42s · 28 turns · tokens: 113.4k in / 9.2k out (+3286.5k cache-read)
📜 Full session transcript
@ -170,2 +145,2 @@"ATPROTO_PDS_ADMIN_PASSWORD environment variable.";}/// <summary>The <see cref="TheoryAttribute"/> counterpart of <see cref="RequiresFactAttribute"/>.</summary>public sealed class RequiresTheoryAttribute : TheoryAttributeRequiresTheoryAttributehas no callers anywhere in the tree (no[RequiresXTheory]existed before this PR either, so nothing is being preserved). Minor, but per the repo's own "don't add features beyond what the task requires" convention, consider dropping it until a theory actually needs it, or note why it's added speculatively.🔧 Let Claude fix this — tick the box and claude-bot picks up the review above, on this PR branch. Nothing runs until you do.