Conversation
TunnelService's CRUD orchestration (create/update/delete/enable/orphan cleanup, ~20 kube API calls across five CRDs) had zero test coverage. Every ProjectControlPlaneClient was built by a real TLS kube::Client against a real cluster URL, so there was no seam to test it against. New crate::fake_apiserver (test-only, #[cfg(test)]) implements tower::Service<http::Request<kube::client::Body>> and is handed directly to kube::Client::new. It understands the REST conventions this crate actually uses against a control plane: list (with the dotted-JSON-path fieldSelector clauses this crate's own selectors use), get, create (honouring metadata.generateName), RFC 7386 JSON merge patch on both the main resource and /status (the only patch strategy Patch::Merge produces), and delete, for both namespaced and cluster-scoped (ConnectorClass) resources. Error responses are shaped like a real apiserver's Status object, which is what kube::Error::Api and this crate's own classify_list_error parse. It does not model RBAC, admission, controllers, or resourceVersion — it is a store, not a cluster. The seam to reach it: ProjectControlPlaneClient now builds its kube::Client through a ClientBuilder closure field instead of calling Self::build_kube_client directly at each call site — production always uses the real TLS builder, but a test can override it. The override lives on DatumCloudClient (a #[cfg(test)] pub(crate) setter, with_test_client_builder), which is where the override needs to live since every TunnelService method reaches its kube client via datum.project_control_plane_client(). The field and its (production- reachable) getter are not cfg-gated, so ProjectControlPlaneClient::new doesn't need its own test-only branch; only the setter is. This means token rotation and the auth-watch background task run through the exact same code as production, just pointed at an in-memory store — the seam swaps out the TLS handshake, nothing else. New tests in tunnels.rs cover create_project (including connector reuse across two tunnels in the same project), list_project_with_orphans, delete_project, set_enabled_project, update_project's no-op-patch idempotency guard (asserted via metadata.generation not moving), and cleanup_orphaned_connectors_project. They build a real ListenNode (same pattern node.rs's own test already uses) rather than mocking iroh. Caveat worth being upfront about: ensure_connector only populates a connector's status.connectionDetails (which find_connector's field selector matches on) once the iroh endpoint has a relay URL, so these tests wait for one the same bounded way bin/src/main.rs already does before its own first connector-touching call. That is a real network dependency on relay reachability, layered on top of fake_apiserver by ListenNode — fake_apiserver itself has none. It resolves in well under a second here; the whole new suite adds about six seconds to the run. This commit was created with the assistance of a LLM.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TunnelService's CRUD orchestration — create/update/delete/enable, orphan cleanup, roughly twenty kube API calls across five CRDs — had zero test coverage. EveryProjectControlPlaneClientwas built by a real TLSkube::Clientagainst a real cluster URL, so there was no seam to test it against.The fake apiserver
New
crate::fake_apiserver(test-only,#[cfg(test)]) implementstower::Service<http::Request<kube::client::Body>>and is handed directly tokube::Client::new. It understands the REST conventions this crate actually uses against a control plane:list, with the dotted-JSON-pathfieldSelectorclauses this crate's own selectors use (status.connectionDetails.publicKey.id=...,spec.connectorRef.name=...)getcreate, honouringmetadata.generateName/status— the only patch strategyPatch::MergeproducesdeleteBoth namespaced and cluster-scoped (
ConnectorClass) resources. Error responses are shaped like a real apiserver'sStatusobject, which is whatkube::Error::Apiand this crate's ownclassify_list_errorparse. It does not model RBAC, admission, controllers, or resourceVersion — it's a store, not a cluster.The seam
ProjectControlPlaneClientnow builds itskube::Clientthrough aClientBuilderclosure field instead of callingSelf::build_kube_clientdirectly at each call site. Production always uses the real TLS builder; a test can override it. The override lives onDatumCloudClient(#[cfg(test)] pub(crate) fn with_test_client_builder), since everyTunnelServicemethod reaches its kube client viadatum.project_control_plane_client(). The field and its getter are notcfg-gated — only the setter is — soProjectControlPlaneClient::newdoesn't need a test-only branch of its own.This means token rotation and the auth-watch background task run through the exact same code as production, just pointed at an in-memory store. The seam only swaps out the TLS handshake.
New tests
In
tunnels.rs:create_project— including connector reuse across two tunnels in the same projectlist_project_with_orphans— separates a referenced connector from a seeded orphandelete_project— removes HTTPProxy, ConnectorAdvertisement, and the connector on its last referenceset_enabled_project— toggling the advertisement's existenceupdate_project's no-op-patch idempotency guard, asserted viametadata.generationnot moving on an unchanged spec, and moving on a real changecleanup_orphaned_connectors_project— deletes an orphan connector and its leftover advertisement, leaves the referenced one aloneThese build a real
ListenNode(the same patternnode.rs's own existing test already uses) rather than mocking iroh.A caveat worth being upfront about
ensure_connectoronly populates a connector'sstatus.connectionDetails— whichfind_connector's field selector matches on — once the iroh endpoint has a relay URL. These tests wait for one the same bounded waybin/src/main.rsalready does before its own first connector-touching call. That's a real network dependency on relay reachability, layered on top offake_apiserverbyListenNode—fake_apiserveritself has none. It resolves in well under a second in this environment; the whole new suite adds about six seconds to the run. If relay egress is ever blocked in CI, these seven tests (not the rest of the suite) would hang up to 10s each before failing.Verification
cargo fmt --all --checkcargo clippy --workspace --all-targets--features integration-testscargo test --workspaceconnect-plugin)Generated by Claude Code