feat(eventhubs): support AMQP-over-WebSockets transport - #5031
feat(eventhubs): support AMQP-over-WebSockets transport#5031Johnathan W (j7nw4r) wants to merge 26 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 2 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 2 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds AMQP-over-WebSockets support to Event Hubs, enabling connections over port 443 while retaining TCP as the default.
Changes:
- Public API: adds
AmqpTransport, connection transport options, and Event Hubs builder methods. - Implements WebSocket connections and configurable rustls-based TLS features.
- Adds transport propagation, tests, documentation, and an end-to-end sample.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
Cargo.toml |
Updates AMQP dependencies and adds TLS dependencies. |
Cargo.lock |
Locks the updated AMQP/WebSocket dependency graph. |
eng/dict/crates.txt |
Registers new dependency names. |
sdk/core/azure_core_amqp/Cargo.toml |
Defines backend, WebSocket, and TLS features. |
sdk/core/azure_core_amqp/CHANGELOG.md |
Documents API, feature, and dependency changes. |
sdk/core/azure_core_amqp/src/connection.rs |
Adds the public transport API. |
sdk/core/azure_core_amqp/src/fe2o3/connection.rs |
Implements TCP TLS and WebSocket connection paths. |
sdk/core/azure_core_amqp/src/fe2o3/error.rs |
Maps WebSocket establishment errors. |
sdk/core/azure_core_amqp/src/lib.rs |
Exports AmqpTransport. |
sdk/eventhubs/azure_messaging_eventhubs/Cargo.toml |
Forwards AMQP transport and TLS features. |
sdk/eventhubs/azure_messaging_eventhubs/CHANGELOG.md |
Documents Event Hubs transport changes. |
sdk/eventhubs/azure_messaging_eventhubs/examples/eventhubs_websocket_transport.rs |
Demonstrates a WebSocket round trip. |
sdk/eventhubs/azure_messaging_eventhubs/src/common/authorizer.rs |
Updates test constructors. |
sdk/eventhubs/azure_messaging_eventhubs/src/common/recoverable/connection.rs |
Propagates transport into AMQP connection options. |
sdk/eventhubs/azure_messaging_eventhubs/src/consumer/event_receiver.rs |
Updates test construction defaults. |
sdk/eventhubs/azure_messaging_eventhubs/src/consumer/mod.rs |
Adds consumer transport selection. |
sdk/eventhubs/azure_messaging_eventhubs/src/event_processor/processor.rs |
Documents inherited consumer transport behavior. |
sdk/eventhubs/azure_messaging_eventhubs/src/models/mod.rs |
Re-exports and documents AmqpTransport. |
sdk/eventhubs/azure_messaging_eventhubs/src/producer/batch.rs |
Updates test construction defaults. |
sdk/eventhubs/azure_messaging_eventhubs/src/producer/mod.rs |
Adds producer transport selection. |
sdk/eventhubs/azure_messaging_eventhubs_checkpointstore_blob/Cargo.toml |
Aligns local core and storage dependencies. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
88ae286 to
3cdffcd
Compare
|
Copilot resolve the merge conflicts in this pull request |
Merged |
Add a transport-type option so AMQP can be tunneled over secure WebSockets (wss://, port 443) for networks that block the native AMQP ports (5671/5672), matching the .NET, Java, and Python Azure SDKs. - azure_core_amqp: add AmqpTransport enum and AmqpConnectionOptions::transport; the fe2o3 backend opens a WebSocketStream and uses open_with_stream for the WebSocket arm while keeping the amqps:// URL for AMQP link addressing. - azure_messaging_eventhubs: add models::TransportType and with_transport_type on the producer and consumer builders. Resolves #3601.
EventProcessor::builder().build() takes a caller-constructed ConsumerClient and reuses its connection for every partition receiver, so the transport is inherited from that client. Document this on build() with an AMQP-over-WebSockets example, and add a test asserting the transport reaches the RecoverableConnection.
Add a runnable sample that opens a producer and a consumer with TransportType::AmqpWebSocket, sends a tagged event, and reads it back. It mirrors the connection-string sample, so the two can be run side by side to compare transports.
Add an IPv6 regression test for the WebSocket address builder, register fe2o3-amqp-ws in the crate spelling dictionary in both the underscore and the hyphen form, and record the AmqpConnectionOptions field addition under Breaking Changes.
Add the native_tls and rustls features, which select the TLS backend for both fe2o3-amqp and fe2o3-amqp-ws. The default feature selects native_tls. The WebSocket transport needs one of them, because fe2o3-amqp-ws gates its TLS entry point behind its own features. Without a backend the crate now compiles and the transport returns an error at run time. Before this change, "--no-default-features --features fe2o3_amqp" did not compile. Mark AmqpConnectionOptions as non_exhaustive and add with_* methods for each field, so later fields are additive. Update the Event Hubs call site to use the new methods.
The rustls backend of fe2o3-amqp depends on ring, and deny.toml bans that crate, so "cargo deny --all-features check bans" failed. Keep native_tls as the only TLS backend and gate the WebSocket transport on it. See issue #4189 for the ring work.
Extract RecoverableConnection::connection_options so a test can assert the transport reaches the options given to AmqpConnection::open. A test on the constructor alone passed even when create_connection dropped the call. Route each builder open path through one transport() helper and test it, which covers the connection-string path too. Correct the CHANGELOG example: "..Default::default()" does not build a non_exhaustive struct from another crate. Show the with_ method instead.
Document on AmqpTransport::WebSocket, on with_transport, and on the Event Hubs TransportType::AmqpWebSocket that the variant needs a TLS backend. A build with default-features = false can select the variant, but the connection then returns an error when it opens.
The Event Hubs crate depends on azure_core_amqp with its default features, so native_tls is always on and the WebSocket variant works in every build of this crate. The previous text described a configuration the manifest does not produce. Also qualify the azure_core_amqp text: the run-time error needs the fe2o3_amqp feature, since the no-op backend panics.
The receiver attach test that came in with #4807 calls RecoverableConnection::new with the previous six arguments, so it stopped compiling after this branch added the transport argument. Pass the default transport.
…mple The open method takes the fully qualified namespace and uses it as the AMQP host, so the short name in the TransportType example does not resolve.
The run-time error needs the fe2o3_amqp feature. Without a backend feature the no-op connection takes over, and its open calls unimplemented!, so it panics.
`AmqpConnectionOptions` returns to a plain struct with public fields. The `#[non_exhaustive]` marker and the `with_` methods are gone, so a struct literal with `..Default::default()` builds it again. The repo allows the `constructible_struct_adds_field` semver lint for this reason. AMQP over WebSockets is now a feature of this crate, not a TLS selection. `websockets_rustls` and `websockets_native_tls` each turn on `fe2o3-amqp-ws` and forward one of its TLS backends. `default` selects `websockets_rustls`, which is rustls with the aws-lc-rs provider, the same stack `azure_core` selects for HTTP. The direct `rustls` dependency selects that provider, because `fe2o3-amqp-ws` takes rustls without default features. The `fe2o3_amqp` feature no longer pulls in `fe2o3-amqp-ws`, so a build with that feature alone compiles. The TCP transport keeps `fe2o3-amqp/native-tls`, which `default` selected before this change: the rustls backend of `fe2o3-amqp` 0.14 pulls in `ring`, which `deny.toml` bans (#4189).
`models::TransportType` duplicated `azure_core_amqp::AmqpTransport`, which this crate already re-exports types from. The enum and its `From` implementation are gone. `models` re-exports `AmqpTransport` beside `AmqpMessage` and `AmqpValue`, and the builder method is `with_transport`, which takes that type.
The WebSocket transport had two features, `websockets_rustls` and `websockets_native_tls`. Each one turned on the transport and one TLS stack in a single step, so a consumer could not select the TLS stack that the rest of the application uses. Give the transport the base plus modifier shape that `azure_core` uses for HTTP, where `reqwest` is the base and `reqwest_rustls` adds the stack. Name the features after the backend crate `fe2o3-amqp-ws`, which is the rule that `reqwest` and the existing `fe2o3_amqp` both follow. `fe2o3_amqp_ws` turns on the transport code, and `fe2o3_amqp_ws_rustls` and `fe2o3_amqp_ws_native_tls` each add one stack on top of it. Add `fe2o3_amqp_native_tls` for the TLS stack of the TCP transport. `default` named `fe2o3-amqp/native-tls` inline before, and `fe2o3-amqp` is only a dev-dependency of `azure_messaging_eventhubs`, so that crate could not forward it. Without the named feature, an application that turned off the default features to select a WebSocket stack also lost the TLS stack of the TCP transport. Forward all five features from `azure_messaging_eventhubs`, in the same way that `azure_core` forwards the `reqwest` TLS features of `typespec_client_core`. That crate is the one an application depends on, so the split was not reachable without the forward. Keep the features additive, which the Cargo reference asks for. A build can hold both WebSocket stacks; `fe2o3-amqp-ws` then uses native-tls, and the connection now logs a warning that names the stack in use.
The rebase onto main brought in new tests that build a `RecoverableConnection` or a `ProducerClient`. Both constructors take the transport, so each new call site needs the argument. The tests do not exercise the transport, so they pass the default, which is TCP.
`fe2o3_amqp_ws_native_tls` was a parity feature: it named a second TLS stack that this crate does not otherwise use. `azure_core` has no such feature. It offers `reqwest` as the base and `reqwest_rustls` for the stack it ships, and `samples/list_blobs_native_tls` selects another stack with a direct `reqwest` dependency and Cargo feature unification. Follow that pattern. Keep `fe2o3_amqp_ws` as the base and `fe2o3_amqp_ws_rustls` for the stack this repository ships. Gate the transport code on the base feature alone, which is what makes unification work: an application can now name `fe2o3_amqp_ws`, add `fe2o3-amqp-ws` with `native-tls`, and get the transport on that stack. The old gate read this crate's own TLS features, so the same manifest compiled the transport out and returned an error at run time. One stack must be selected somewhere in the graph, because `fe2o3-amqp-ws` puts `connect_tls_with_config` behind its own TLS features. `fe2o3_amqp_ws` on its own therefore does not build. `reqwest` differs here, since it compiles with no TLS feature at all. Add `tokio-tungstenite` to the crate dictionary, in both spellings, beside the other entries.
`fe2o3-amqp` 0.16 builds its rustls backend on aws-lc-rs, where 0.14 built it on `ring`, which `deny.toml` bans. That was the only reason this crate named native-tls for AMQP framed on TCP. Update the `fe2o3-amqp` family from 0.14 to 0.16, which needs no source change, and replace `fe2o3_amqp_native_tls` with `fe2o3_amqp_rustls`. The `default` feature selects it, so both the TCP and the WebSocket transport now run on rustls with the aws-lc-rs provider, the stack that the rest of `sdk/core` uses. There is no native-tls feature. `fe2o3-amqp` accepts one TLS stack and reports `TlsConnectorNotFound` at run time when both are on, so a pair of features that cannot both be on would break `--all-features`. A consumer that wants another stack turns off the default features and takes a direct dependency on `fe2o3-amqp` with the stack they want, which is the pattern `samples/list_blobs_native_tls` shows for `reqwest`. The `azure_core_amqp` dependency of `azure_messaging_eventhubs` now sets `default-features = false`, in the same shape as `azure_core`, so that turning the default features off can drop the rustls stack. Closes #4189.
`fe2o3_amqp_ws` is the base feature and names no TLS stack, but the transport called `WebSocketStream::connect_tls_with_config`, which `fe2o3-amqp-ws` puts behind its own TLS features. A build that named `fe2o3_amqp_ws` alone therefore failed with E0599, so the advertised base feature did not compile. Call `connect_with_config` instead. It carries no feature gate, and it reaches the same `tokio_tungstenite` connect path with no connector, which is what the previous call passed. The behavior is unchanged when a TLS stack is selected, and a build that selects none now reports `TlsFeatureNotEnabled` when the connection opens. This matches `reqwest`, which also compiles with no TLS feature and fails at run time, and it needs no TLS modifier feature of its own. Update the feature comment, the `AmqpTransport::WebSocket` doc, and the changelog, which all said the base feature could not build.
`RecoverableConnection::new` takes a `transport` argument on this branch, and `ConsumerClientOptions` carries a `transport` field. PR #4933 landed on main after this branch forked and added a unit test and a builder call that use the earlier shapes. Git merges the two changes without a textual conflict, so the branch built on its own while the pipeline, which builds the merge with main, failed with E0061, E0063, and E0277. Pass the default transport at both new call sites.
The `docs.rs` metadata named `fe2o3_amqp`, `fe2o3_amqp_ws`, and `fe2o3_amqp_ws_rustls`, so docs.rs built the crate without the TLS stack for AMQP framed on TCP. Add `fe2o3_amqp_rustls`. The list now holds every feature that `default` selects. It leaves out `test`, which is an internal helper, and `ffi`, which does not build on its own (#4978). `azure_core` names its features the same way.
The comment said the receiver and the stream hold references to the consumer's connection, so the caller must release them before the consumer closes. That overstates the requirement. `ConsumerClient::close` takes `&self` on the connection manager and drains the receiver cache, so it detaches the receiver link on its own. A receiver that outlives its client then reports the closed client instead of opening a second connection (#4931). The order of `receiver.close()` and `consumer.close()` is free. The one order the caller must keep is the stream before the receiver. `stream_events` borrows `&self` and `close` takes `self` by value, so the compiler enforces it. The block above already covers that.
Two entries under Features Added are breaking. Restore the section and move them. The `AmqpConnectionOptions::transport` field lands on a struct that is not `#[non_exhaustive]`, so an existing struct literal that names every field no longer compiles. The `default` feature swap from `fe2o3-amqp/native-tls` to `fe2o3_amqp_rustls` changes the trust anchors. `fe2o3-amqp` builds its rustls root store from `webpki-roots`, and native-tls read the root store of the operating system. The same swap reaches `azure_messaging_eventhubs` through its forwarded features, so its changelog gets the matching entry.
An explicit port on `custom_endpoint` carries into the `wss://` address, so a proxy named as `amqps://proxy:5671` dials 5671 and not 443. The .NET Azure SDK does the same. `AmqpClient` builds its `ConnectionEndpoint` from the host and the explicit port of `CustomEndpointAddress`, and `CreateTransportSettingsForWebSockets` carries that port into the WebSocket URI. A proxy that accepts WebSockets on its own port stays reachable, so the behavior stands. Document the rule on `AmqpConnectionOptions::custom_endpoint` and on the `with_custom_endpoint` builders, and pin the AMQP-port case with a test.
The `rustls` feature of `fe2o3-amqp` 0.16 fills its root store from `webpki-roots` only, which is a compiled-in copy of the Mozilla root set. The connection called `open()` with no connector, so AMQP framed directly on TCP took that default and stopped reading the trust store of the operating system. A broker behind a private or an enterprise certificate authority would then fail the handshake, even when the operating system trusts that authority. Supply a connector built on `rustls-platform-verifier` instead. The TCP transport now trusts the same roots as `reqwest/rustls` on the HTTP side, and it agrees with the WebSocket transport, which already reads the same store through native roots. Use `rustls_connector`, because it needs the `rustls` feature only, where `tls_connector` also needs `native-tls` to be off. The connector-typed builder hands `alt_tls_estab` to the same transport call as the default path, so `alt_tls_establishment(true)` keeps its behavior. Add a test that builds the connector, which catches a missing crypto provider at test time instead of at connect time.
The Event Hubs breaking-change entry still described the webpki-roots behavior that the platform verifier replaced, so it warned about a handshake failure behind a private certificate authority that no longer happens. It now matches the azure_core_amqp entry. The AmqpTransport::WebSocket documentation promised an error when the connection opens without the fe2o3_amqp_ws feature. A build that names no backend feature gets NoopAmqpConnection, whose open panics, so the statement now covers both cases.
0d5442b to
2061b87
Compare
Supersedes #4596. That pull request came from a fork. Fork pull requests cannot authenticate to the Azure Artifacts cargo feed that #4928 added, so the pipeline failed before it compiled anything. This branch lives in the
Azurerepository, so the pipeline authenticates normally. The code is unchanged, and the review history stays on #4596.Summary
azure_messaging_eventhubsconnected over AMQP on TCP/TLS only (port 5671). No option selected AMQP-over-WebSockets (wss://, port 443). Some networks block the native AMQP ports (5671 and 5672). Clients on those networks had no path to Event Hubs. The .NET, Java, and Python Azure SDKs all offer this option. (#3601)This change adds a transport option, and it keeps the TLS stack a separate choice. Both parts are additive. No existing caller breaks, and
Tcpstays the default.Motivation
The AMQP WebSocket binding sends an AMQP connection over a WebSocket on port 443. The service treats that connection as equal to a 5671 connection. The client builder and the AMQP backend fixed the scheme to
amqps://and the transport to TCP, so no public option reached the WebSocket binding. Some firewalls block every outbound port except 443. Clients behind those firewalls cannot reach Event Hubs without a transport selector.A WebSocket connection also needs a TLS stack. Most applications already link one stack. A second stack adds binary size, and it adds a second trust store to configure.
azure_corekeeps the transport and the stack apart for HTTP, wherereqwestis the base feature andreqwest_rustlsadds the stack. A consumer of the AMQP crates expects the same shape.The TLS stack also controls which root certificates the client trusts. A change of stack must keep the anchors that the previous stack used, or a broker behind a private certificate authority stops working.
Changes
azure_core_amqpadds the publicAmqpTransport { Tcp, WebSocket }enum and anAmqpConnectionOptions::transportfield.AmqpConnectionOptionsstays a plain options bag with public fields, so..Default::default()still builds it. Theopen()function of thefe2o3backend branches on the transport. TheWebSocketarm connects afe2o3_amqp_ws::WebSocketStream, and it gives the stream toConnection::builder().open_with_stream(...). The AMQPhostnamestays the real service host, so AMQP link addressing does not change. SASL ANONYMOUS and CBS authorization stay the same.WebSocketarm callsconnect_with_config, notconnect_tls_with_config.fe2o3-amqp-wsputsconnect_tls_with_configbehind its own TLS features, and a call to it would keepfe2o3_amqp_wsfrom building on its own.connect_with_configcarries no such gate. It uses whichever stack the features offe2o3-amqp-wsselect, and the connection reportsTlsFeatureNotEnabledwhen the graph selects none.fe2o3_amqpis the base backend.fe2o3_amqp_rustlsadds the TLS stack for AMQP framed on TCP.fe2o3_amqp_wsturns on the WebSocket transport code and thefe2o3-amqp-wsdependency, and it names no TLS stack.fe2o3_amqp_ws_rustlsadds the stack for the WebSocket transport.defaultselects all four. Each stack is rustls with the aws-lc-rs provider, which is the stack that the rest ofsdk/coreuses.fe2o3_amqp_rustlsbuilds onrustls-platform-verifier. The default connector offe2o3-amqpfills its root store fromwebpki-roots, which is a compiled-in copy of the Mozilla root set, and it ignores the trust store of the operating system. native-tls read that store before this change, so the default would drop the roots that an operator installs, and a broker behind a private or an enterprise certificate authority would fail the handshake. The platform verifier reads the trust store of the operating system on every supported target, through Security.framework on Apple, through CryptoAPI on Windows, and throughrustls-native-certselsewhere. The TCP transport therefore keeps the anchors it had, and it agrees withreqwest/rustlson the HTTP side and with the WebSocket transport, which reads the same store through native roots.webpki-rootsstill arrives as a dependency, because therustlsfeature offe2o3-amqpalways names it, and nothing reads it.rustls_connector, not through thetls_connectoralias.rustls_connectorneeds therustlsfeature only, where the alias also needsnative-tlsto be off, so the call survives a consumer that unifies both features. The connector-typed builder keeps its ownopen, and that path handsalt_tls_estabto the same transport call as the default path, soalt_tls_establishment(true)keeps its behavior.fe2o3-amqpaccepts one TLS stack only, and it reportsTlsConnectorNotFoundat run time when both are on. Two features that cannot both be on would break--all-features. To build on another stack, turn off the default features, name the base features, and take a direct dependency onfe2o3-amqpandfe2o3-amqp-wswith the stack you want. Cargo unifies the features, and nothing pulls rustls in.samples/list_blobs_native_tlsshows the same pattern forreqwest. The feature comments record this recipe.fe2o3_amqp_rustlsandfe2o3_amqp_ws_rustlseach take a directrustlsdependency. That dependency only selects the crypto provider, and this crate names one rustls type, which is theClientConfigthat the connector needs. Thetokio-tungstenitechain underfe2o3-amqp-wstakes rustls withdefault-features = falseand turns on no provider feature, soClientConfig::builder()panics when no process-level default is installed. The TCP connector calls the same builder and needs the same guarantee. The default features ofrustlsgive aws-lc-rs, std, and tls12, anddeny.tomlbansring, so this repository cannot reach that panic. An application that unifies a second provider into the graph must callCryptoProvider::install_default()first.fe2o3-amqpfamily from 0.14 to 0.16, and it addsfe2o3-amqp-ws0.16,rustls0.23,rustls-platform-verifier0.7, andtokio-rustls0.26. The rustls backend offe2o3-amqp0.14 was built onring, whichdeny.tomlbans. Version 0.16 uses aws-lc-rs instead, so this change closes Cannot use aws-lc-rs for AMQP #4189.reqwestalready resolvesrustls-platform-verifierandtokio-rustlsthrough its ownrustlsfeature, so the graph gains no new package, andCargo.lockgains two dependency edges only.azure_messaging_eventhubsforwards all four features.azure_coreforwards thereqwestTLS features oftypespec_client_corein the same way. An application depends on the Event Hubs crate, so the split does not reach the application without the forward.azure_messaging_eventhubsre-exportsAmqpTransportasmodels::AmqpTransport, next to the otherazure_core_amqptypes that this crate already re-exports. It addswith_transport(...)on the producer builder and the consumer builder. The transport reaches the connection on all four construction paths, which areopenandopen_with_connection_stringon each of the two builders.EventProcessortakes the transport from theConsumerClientthatbuildreceives. It therefore runs over WebSockets when that client selects them. The documentation onbuildshows this with an example.wss://{host}[:{port}]/$servicebus/websocket/. The other Azure SDKs use the same binding path. A custom endpoint redirects the socket, and the AMQPhostnamestays the real service host.eventhubs_websocket_transportsample opens a producer and a consumer over WebSockets, sends a tagged event, and reads the event back.docs.rsmetadata names all four features, so docs.rs documents both transports together with their TLS stacks.azure_messaging_eventhubsneeds the unreleasedAmqpTransport. It therefore takesazure_core_amqpandazure_coreas localpathplusversiondependencies, and it takes the local dev-dependencies aspathonly. This follows the policy inAGENTS.md.azure_messaging_eventhubs_checkpointstore_blobgets the same treatment. Without those lines, the registry build ofazure_storage_blobpins the previousazure_core, and two copies of the crate land in the graph. Two copies break the trait bounds that cross between the two crates. These lines return toworkspace = truewhenazure_core1.2.0 publishes.Related work
#4873 adds
BufferedProducerClient, which gives the crate a third public builder.BufferedProducerClientBuilderforwardsapplication_id,retry_options, andcustom_endpointtoProducerClient::builder(). It needs the same forward for the transport that this change adds. Without that one method, a buffered producer cannot use AMQP over WebSockets. Neither change blocks the other. The second change to merge adds the method.Both changes edit
producer/mod.rs,common/recoverable/connection.rs, and the changelog. The second change to merge therefore needs a small rebase. #4895 also editscommon/recoverable/connection.rs.Test plan
Five feature combinations of
azure_core_amqpbuild, and each build passes--no-default-features. The feature sets arefe2o3_amqp;fe2o3_amqpwithfe2o3_amqp_rustls;fe2o3_amqpwithfe2o3_amqp_ws;fe2o3_amqpwithfe2o3_amqp_wsandfe2o3_amqp_ws_rustls; and all four together. The default build passes, and--all-features --all-targetspasses. The base-only build namesfe2o3_amqp_wswith no TLS modifier, which is the combination that must compile without a stack.The unit tests pass.
azure_core_amqpruns 122 tests.azure_messaging_eventhubsruns 148 tests, and it ignores 14 live tests. 51 doc tests pass, 4 in the first crate and 47 in the second. The tests cover the WebSocket address construction, and they include an IPv6 case. They also cover the transport that reachesRecoverableConnection, the options thatAmqpConnection::openreceives, and the builder helper that everyopenpath reads. One test builds the TCP connector, which reports a missing crypto provider at test time instead of at connect time.cargo fmt --checkpasses, andcargo clippy --all-features --all-targetspasses. cspell passes against the repository configuration.cargo treeshows noringin the default graph.A live run against an Event Hubs namespace validates both transports. Three connection-string tests passed over TCP, which are
test_round_trip_connection_string,send_eventdata_with_connection_string, andconsumer_open_with_connection_string. A temporary test then opened a producer on each transport, read the hub properties, and sent an event. Both transports passed. Theeventhubs_websocket_transportsample opened a producer and a consumer withAmqpTransport::WebSocket, sent a tagged event, and read the event back. The process held one established socket during that run, to port 443. Theeventhubs_connection_stringsample keeps the default transport, and it ran against the same namespace as a control. That process held its socket on port 5671.A negative control proves that the supplied connector governs the handshake. The service certificate chains to a public authority, and both root sets carry that authority, so a passing test alone does not separate the new connector from the default connector. A temporary build replaced the platform verifier with an empty root store. The live round trip then failed with
invalid peer certificate: UnknownIssuer, and it passed again after the revert.A run from a network that blocks 5671 outbound is still open. A broker behind a private certificate authority has the negative control only, because the test namespace uses a public authority. An application that selects another TLS stack has build coverage only.