connectrpc-testing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ace3966903eb60c52dd074d1204311c7903f41c4a6d726ea3831e844962631cd
4
+ data.tar.gz: d4e8e92888ba3206f2801bd27a7e1e99a04644cc78fc116167cb899d018ddf1a
5
+ SHA512:
6
+ metadata.gz: 53a9fbfabd5692eeea6c8b6ca162f29a1e75a72e58bb5ea849d13839f963c8542aca9f5173a3589674a13867cb8aedbba11cef4ff2332bb6003abdc95ef262bf
7
+ data.tar.gz: 86e731569e68980795a8f608e15b799878bc50a440bbbeb5b24aa87338db0cb121986038adafbf21d1087272d4350d3cfe61b49cfebbdf9aecadcae055d93b47
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jim Myhrberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # connectrpc-testing
2
+
3
+ A synchronous in-process transport for real Connect unary round trips. The
4
+ application supplies a core `Server`, its binding, services and handlers. The
5
+ ordinary core or generated client still encodes requests and decodes responses,
6
+ including protocol errors, metadata, safe unary GET and compression. No sockets are opened.
7
+
8
+ Add `gem "connectrpc-testing", path: "gems/connectrpc-testing"` to a development
9
+ or test bundle in this repository. For another local application, point `path:`
10
+ at this package and provide the core gem through its own local path. Released
11
+ versions are published to RubyGems.org with the whole
12
+ [release train](../../docs/releasing.md). The runtime dependency is only `connectrpc ~> 0.1.0`; Google protobuf
13
+ and a test framework are application choices.
14
+
15
+ ```ruby
16
+ require "connectrpc/testing"
17
+ require "connectrpc/bindings/google_protobuf" # application's optional binding
18
+ require "echo_connect" # application's generated messages and service
19
+
20
+ service = Example::V1::EchoService
21
+ binding = ConnectRPC::Bindings::GoogleProtobuf.new
22
+ server = ConnectRPC::Server.new(
23
+ services: [service::SERVICE], binding: binding,
24
+ handlers: service.handlers(
25
+ echo: ->(request, context) {
26
+ context.response_trailers.add("x-tested", "yes")
27
+ request
28
+ },
29
+ fail: ->(_request, _context) {
30
+ raise ConnectRPC::Error.new(:invalid_argument, "safe message")
31
+ }
32
+ )
33
+ )
34
+ transport = ConnectRPC::Testing::Transport.new(server: server)
35
+ core = ConnectRPC::Client.new(
36
+ base_url: "https://in-process.invalid", binding: binding, transport: transport
37
+ )
38
+ client = service::Client.new(client: core)
39
+ response = client.echo(Example::V1::EchoMessage.new(name: "hello"), timeout: nil)
40
+ raise "Unexpected response" unless response.message.name == "hello"
41
+ raise "Missing trailer" unless response.trailers["x-tested"] == ["yes"]
42
+ ```
43
+
44
+ The hostname is validated but never resolved. URLs must be absolute HTTP(S) URIs
45
+ without credentials or fragments. Raw URI queries are forwarded to core, including safe unary GET. The literal URI path
46
+ identifies the method: a base URL prefix is retained and will normally produce
47
+ core's unknown-method response. There is no mount-prefix stripping or Rack
48
+ fallthrough. The actual HTTP method and repeated leading header arrays pass to
49
+ core unchanged. Malformed protocol header values retain core classification;
50
+ structurally unsupported Ruby inputs raise `ArgumentError` before dispatch.
51
+ Frozen caller URI, headers and body are supported and preserved.
52
+
53
+ The private byte reader uses `byteslice` over the original body under core's
54
+ bounded `MessageIO` reads, including multibyte JSON. It does not decode or
55
+ recompress payloads. `TransportRequest.response_limit` caps encoded bytes,
56
+ including error bodies and gzip framing. Core's server materializes a response
57
+ before the transport checks its cap. That check cannot undo the allocation and
58
+ does not establish a total memory bound. Server and Client message and wire
59
+ limits remain separate.
60
+
61
+ A supplied original `Deadline` is checked before and after dispatch. Immediately
62
+ before dispatch, the transport copies headers and refreshes `connect-timeout-ms`
63
+ from the remaining budget. Core reconstructs its cooperative server deadline
64
+ from rounded/clamped wire milliseconds. The original deadline remains
65
+ authoritative and expiry takes precedence over the response wire cap. The
66
+ Client's omitted timeout remains 30 seconds; `timeout: nil` adds no deadline or
67
+ timeout header. A low-level request with nil deadline can still explicitly
68
+ supply a core timeout header.
69
+
70
+ Execution is synchronous and cooperative. This transport cannot interrupt a
71
+ blocked handler or codec. It makes no network, DNS, TLS, fiber, backpressure,
72
+ concurrency, lifecycle, retry or cancellation compatibility promise. Use real
73
+ hosts and independent peers for those checks. These round trips are not
74
+ interoperability or official conformance evidence.
75
+
76
+ From the repository root:
77
+
78
+ ```sh
79
+ mise run testing:setup
80
+ mise run testing:test -- spec/transport_spec.rb
81
+ mise run testing:check
82
+ mise run testing:example
83
+ mise run testing:package
84
+ MISE_RUBY_VERSION=4.0.9 mise run testing:verify
85
+ ```
86
+
87
+ Pinned Ruby and Bundler must already be installed; these tasks never install
88
+ Bundler globally. The independent checksummed bundle, caches and archives use
89
+ `vendor/testing/`; SimpleCov starts before library requires, includes every
90
+ package library file and writes `coverage/testing/<Ruby>/`. `testing:check`
91
+ parses every maintained Ruby file and uses the repository RuboCop policy.
92
+ `testing:package` builds its own core archive, installs only the testing archive
93
+ with normal offline resolution from an adjacent core archive in a fresh external
94
+ GEM_HOME, checks public require without optional bindings/hosts, then installs
95
+ the cached Google dependency closure and runs the generated example in both
96
+ codecs and encodings. Root verification remains independent.
97
+
98
+ Licensed under the [MIT License](LICENSE). It is released with the whole
99
+ [release train](../../docs/releasing.md); its package tasks never publish it.
100
+
101
+ See the [supported 0.x contract](../../docs/support.md) for the target support
102
+ profile, matched package versions, compatibility policy and adoption gates.
103
+
104
+ ## RPC observation
105
+
106
+ Configure `instrumentation:` on the core Client/Server to enable the fixed
107
+ neutral per-call lifecycle or the optional [OpenTelemetry adapter](../connectrpc-otel/README.md).
108
+ The server always extracts serialized propagation headers, even while a client
109
+ context is ambient. Direct processing ends with delivery `unobserved`.
110
+
111
+ ## Static signatures
112
+
113
+ This package owns `sig/` and its discovery manifest. Add
114
+ `library "connectrpc-testing"` to a strict Steep target with this gem installed.
115
+ See the [RBS guide](../../docs/rbs.md) for supported APIs, extension contracts,
116
+ partial records, runtime validation and framework-signature limits.
117
+ `mise run rbs:check` checks application callers for this package; strict core
118
+ source checking is a separate target from backend implementation checking.
119
+
120
+ ## Explicit unary cancellation
121
+
122
+ Pass `cancellation: source.token` to core or generated client calls, where
123
+ `source = ConnectRPC::CancellationSource.new`. The source is permanent and
124
+ shareable; only `source.cancel` initiates cancellation. `timeout: nil` keeps no
125
+ RPC deadline or wire timeout. Context offers cooperative `cancelled?`,
126
+ `cancellation_reason` and `check!`. See the [core guide](../../README.md) for
127
+ listener, arbitration and registration lifetime rules.
128
+
129
+ Testing forwards the read-only cancellation seam into a distinct server Context.
130
+ Completing server processing cannot complete its client or shared source. Checks
131
+ are cooperative; Testing does not interrupt a blocked handler.
132
+
133
+ ## gRPC-Web
134
+
135
+ Manual encoded TransportRequest values can exercise explicitly enabled binary/text unary gRPC-Web. This adds no Web Client mode, serializer, network or browser conformance claim.
136
+ See the [Web guide](../../docs/grpc-web.md) for protocol, CORS, bounds and completion rules.
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Testing
5
+ class Transport
6
+ def initialize(server:)
7
+ raise ArgumentError, "server must be a ConnectRPC::Server" unless server.is_a?(Server)
8
+
9
+ @server = server
10
+ end
11
+
12
+ def call(request)
13
+ validate!(request)
14
+ CallCancellation.with_request(request) { |outgoing| perform_call(outgoing) }
15
+ end
16
+
17
+ private
18
+
19
+ def perform_call(request)
20
+ request.cancellation ? request.cancellation.check! : request.deadline&.check!
21
+ response = dispatch(request)
22
+ request.cancellation ? request.cancellation.check! : request.deadline&.check!
23
+ Protocol.check_size!(response.body, request.response_limit)
24
+ response
25
+ end
26
+
27
+ def dispatch(request)
28
+ headers = request.headers.dup
29
+ headers["connect-timeout-ms"] = [request.deadline.timeout_ms] if request.deadline
30
+ @server.call(path: request.url.path, http_method: request.http_method,
31
+ headers: headers, input: BytesReader.new(request.body), query: request.url.query || "",
32
+ cancellation: request.cancellation)
33
+ end
34
+
35
+ def validate!(request)
36
+ raise ArgumentError, "request must be a ConnectRPC::TransportRequest" unless request.is_a?(TransportRequest)
37
+ unless valid_url?(request.url)
38
+ raise ArgumentError,
39
+ "url must be an absolute HTTP(S) URI without credentials or fragment"
40
+ end
41
+
42
+ validate_fields!(request)
43
+ unless request.deadline.nil? || request.deadline.is_a?(Deadline)
44
+ raise ArgumentError, "deadline must be a ConnectRPC::Deadline or nil"
45
+ end
46
+
47
+ Protocol.limit!(request.response_limit)
48
+ end
49
+
50
+ def validate_fields!(request)
51
+ unless request.http_method.is_a?(String) && request.body.is_a?(String)
52
+ raise ArgumentError, "method and body must be Strings"
53
+ end
54
+ return if valid_headers?(request.headers)
55
+
56
+ raise ArgumentError, "headers must contain lower-case String keys and String arrays"
57
+ end
58
+
59
+ def valid_url?(url)
60
+ url.is_a?(URI::HTTP) && %w[http https].include?(url.scheme) && url.host && !url.host.empty? &&
61
+ !url.userinfo && !url.fragment
62
+ end
63
+
64
+ def valid_headers?(headers)
65
+ headers.is_a?(Hash) && headers.all? do |key, values|
66
+ key.is_a?(String) && key == key.downcase && values.is_a?(Array) && values.all?(String)
67
+ end
68
+ end
69
+
70
+ # MessageIO controls read sizes. Offsets are bytes even for UTF-8 JSON.
71
+ class BytesReader
72
+ def initialize(bytes)
73
+ @bytes = bytes
74
+ @offset = 0
75
+ end
76
+
77
+ def read(length)
78
+ return if @offset >= @bytes.bytesize
79
+
80
+ chunk = @bytes.byteslice(@offset, length)
81
+ @offset += chunk.bytesize
82
+ chunk
83
+ end
84
+ end
85
+ private_constant :BytesReader
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Testing
5
+ VERSION = "0.1.0" # x-release-please-version
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "connectrpc"
4
+ require_relative "testing/version"
5
+ require "uri"
6
+ require_relative "testing/transport"
@@ -0,0 +1,9 @@
1
+ module ConnectRPC
2
+ module Testing
3
+ VERSION: String
4
+ class Transport
5
+ def initialize: (server: Server) -> void
6
+ def call: (TransportRequest) -> TransportResponse
7
+ end
8
+ end
9
+ end
data/sig/manifest.yaml ADDED
@@ -0,0 +1,2 @@
1
+ dependencies:
2
+ - name: connectrpc
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: connectrpc-testing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jimeh
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-11-15 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: connectrpc
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
26
+ description: Optional synchronous testing transport using an application-owned core
27
+ Server.
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE
33
+ - README.md
34
+ - lib/connectrpc/testing.rb
35
+ - lib/connectrpc/testing/transport.rb
36
+ - lib/connectrpc/testing/version.rb
37
+ - sig/connectrpc-testing.rbs
38
+ - sig/manifest.yaml
39
+ homepage: https://github.com/jimeh/connectrpc-ruby
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ allowed_push_host: https://rubygems.org
44
+ rubygems_mfa_required: 'true'
45
+ source_code_uri: https://github.com/jimeh/connectrpc-ruby
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: In-process transport for real ConnectRPC unary round trips
63
+ test_files: []