spikard 0.7.5 → 0.8.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/ext/spikard_rb/Cargo.lock +583 -201
  3. data/ext/spikard_rb/Cargo.toml +1 -1
  4. data/lib/spikard/grpc.rb +182 -0
  5. data/lib/spikard/version.rb +1 -1
  6. data/lib/spikard.rb +1 -0
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +2 -1
  8. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +197 -0
  9. data/vendor/crates/spikard-bindings-shared/src/lib.rs +2 -0
  10. data/vendor/crates/spikard-core/Cargo.toml +1 -1
  11. data/vendor/crates/spikard-http/Cargo.toml +5 -1
  12. data/vendor/crates/spikard-http/src/grpc/handler.rs +260 -0
  13. data/vendor/crates/spikard-http/src/grpc/mod.rs +342 -0
  14. data/vendor/crates/spikard-http/src/grpc/service.rs +392 -0
  15. data/vendor/crates/spikard-http/src/grpc/streaming.rs +237 -0
  16. data/vendor/crates/spikard-http/src/lib.rs +14 -0
  17. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +288 -0
  18. data/vendor/crates/spikard-http/src/server/mod.rs +1 -0
  19. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +1023 -0
  20. data/vendor/crates/spikard-http/tests/common/mod.rs +8 -0
  21. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +653 -0
  22. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +332 -0
  23. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +518 -0
  24. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +476 -0
  25. data/vendor/crates/spikard-rb/Cargo.toml +2 -1
  26. data/vendor/crates/spikard-rb/src/config/server_config.rs +1 -0
  27. data/vendor/crates/spikard-rb/src/grpc/handler.rs +352 -0
  28. data/vendor/crates/spikard-rb/src/grpc/mod.rs +9 -0
  29. data/vendor/crates/spikard-rb/src/lib.rs +4 -0
  30. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  31. metadata +15 -1
@@ -0,0 +1,1023 @@
1
+ //! gRPC test utilities and helpers for comprehensive integration tests
2
+ //!
3
+ //! This module provides reusable utilities for writing gRPC integration tests without
4
+ //! requiring language bindings. It includes helpers for creating test servers, clients,
5
+ //! building protobuf messages, and asserting on responses.
6
+ //!
7
+ //! # Examples
8
+ //!
9
+ //! ```ignore
10
+ //! use common::grpc_helpers::{
11
+ //! GrpcTestServer, create_grpc_test_client, send_unary_request,
12
+ //! assert_grpc_response, create_test_metadata, ProtobufMessageBuilder,
13
+ //! };
14
+ //! use std::sync::Arc;
15
+ //! use bytes::Bytes;
16
+ //!
17
+ //! #[tokio::test]
18
+ //! async fn test_grpc_service() {
19
+ //! let mut server = GrpcTestServer::new();
20
+ //! let metadata = create_test_metadata();
21
+ //!
22
+ //! let message = ProtobufMessageBuilder::new()
23
+ //! .add_field("name", "Alice")
24
+ //! .add_field("age", 30)
25
+ //! .build()
26
+ //! .unwrap();
27
+ //!
28
+ //! let response = send_unary_request(
29
+ //! &server,
30
+ //! "mypackage.UserService",
31
+ //! "GetUser",
32
+ //! message,
33
+ //! metadata,
34
+ //! )
35
+ //! .await
36
+ //! .unwrap();
37
+ //!
38
+ //! assert_grpc_response(response, serde_json::json!({"id": 1}));
39
+ //! }
40
+ //! ```
41
+
42
+ use bytes::Bytes;
43
+ use serde_json::{json, Value};
44
+ use spikard_http::grpc::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
45
+ use std::future::Future;
46
+ use std::pin::Pin;
47
+ use std::sync::Arc;
48
+ use tonic::metadata::MetadataMap;
49
+ use tonic::Code;
50
+
51
+ /// Test server for gRPC integration testing
52
+ ///
53
+ /// Provides a simple in-memory server for registering test handlers and managing
54
+ /// their lifecycle. This is useful for testing gRPC services without spinning up
55
+ /// a real network server.
56
+ ///
57
+ /// # Example
58
+ ///
59
+ /// ```ignore
60
+ /// let mut server = GrpcTestServer::new();
61
+ /// server.register_service(Arc::new(my_handler));
62
+ /// assert!(!server.handlers().is_empty());
63
+ /// ```
64
+ #[derive(Clone)]
65
+ pub struct GrpcTestServer {
66
+ handlers: Arc<std::sync::Mutex<Vec<Arc<dyn GrpcHandler>>>>,
67
+ base_url: String,
68
+ }
69
+
70
+ impl GrpcTestServer {
71
+ /// Create a new gRPC test server
72
+ ///
73
+ /// Initializes an empty test server with a default localhost base URL.
74
+ pub fn new() -> Self {
75
+ Self {
76
+ handlers: Arc::new(std::sync::Mutex::new(Vec::new())),
77
+ base_url: "http://localhost:50051".to_string(),
78
+ }
79
+ }
80
+
81
+ /// Create a new gRPC test server with a custom base URL
82
+ ///
83
+ /// # Arguments
84
+ ///
85
+ /// * `url` - The base URL for the server (e.g., "http://localhost:8080")
86
+ pub fn with_url(url: impl Into<String>) -> Self {
87
+ Self {
88
+ handlers: Arc::new(std::sync::Mutex::new(Vec::new())),
89
+ base_url: url.into(),
90
+ }
91
+ }
92
+
93
+ /// Register a gRPC service handler with the test server
94
+ ///
95
+ /// # Arguments
96
+ ///
97
+ /// * `handler` - Arc-wrapped handler implementation to register
98
+ ///
99
+ /// # Example
100
+ ///
101
+ /// ```ignore
102
+ /// let mut server = GrpcTestServer::new();
103
+ /// server.register_service(Arc::new(MyHandler));
104
+ /// ```
105
+ pub fn register_service(&mut self, handler: Arc<dyn GrpcHandler>) {
106
+ let mut handlers = self.handlers.lock().unwrap();
107
+ handlers.push(handler);
108
+ }
109
+
110
+ /// Get the base URL of the test server
111
+ pub fn url(&self) -> &str {
112
+ &self.base_url
113
+ }
114
+
115
+ /// Get all registered handlers
116
+ ///
117
+ /// Useful for inspecting which handlers have been registered.
118
+ pub fn handlers(&self) -> Vec<Arc<dyn GrpcHandler>> {
119
+ let handlers = self.handlers.lock().unwrap();
120
+ handlers.clone()
121
+ }
122
+
123
+ /// Find a handler by service name
124
+ ///
125
+ /// # Arguments
126
+ ///
127
+ /// * `service_name` - The fully qualified service name to look up
128
+ pub fn get_handler(&self, service_name: &str) -> Option<Arc<dyn GrpcHandler>> {
129
+ let handlers = self.handlers.lock().unwrap();
130
+ handlers
131
+ .iter()
132
+ .find(|h| h.service_name() == service_name)
133
+ .cloned()
134
+ }
135
+
136
+ /// Check if a service is registered
137
+ ///
138
+ /// # Arguments
139
+ ///
140
+ /// * `service_name` - The service name to check
141
+ pub fn has_service(&self, service_name: &str) -> bool {
142
+ self.get_handler(service_name).is_some()
143
+ }
144
+
145
+ /// Get the count of registered services
146
+ pub fn service_count(&self) -> usize {
147
+ self.handlers.lock().unwrap().len()
148
+ }
149
+ }
150
+
151
+ impl Default for GrpcTestServer {
152
+ fn default() -> Self {
153
+ Self::new()
154
+ }
155
+ }
156
+
157
+ /// Create a simple gRPC test client for testing
158
+ ///
159
+ /// In real-world scenarios, you would use a proper gRPC client library like
160
+ /// `tonic::Client`. This helper creates a lightweight wrapper for testing
161
+ /// without requiring actual network connections.
162
+ ///
163
+ /// # Example
164
+ ///
165
+ /// ```ignore
166
+ /// let _client = create_grpc_test_client();
167
+ /// // Use with send_unary_request
168
+ /// ```
169
+ pub struct GrpcTestClient;
170
+
171
+ impl GrpcTestClient {
172
+ /// Create a new test client
173
+ pub fn new() -> Self {
174
+ Self
175
+ }
176
+ }
177
+
178
+ impl Default for GrpcTestClient {
179
+ fn default() -> Self {
180
+ Self::new()
181
+ }
182
+ }
183
+
184
+ /// Factory function to create a gRPC test client
185
+ pub fn create_grpc_test_client() -> GrpcTestClient {
186
+ GrpcTestClient::new()
187
+ }
188
+
189
+ /// Send a unary gRPC request to a test server
190
+ ///
191
+ /// This is a helper function to simulate sending a unary RPC request to a gRPC service.
192
+ /// In a real test, you would use a proper gRPC client library, but this helper
193
+ /// allows testing the handler logic directly.
194
+ ///
195
+ /// # Arguments
196
+ ///
197
+ /// * `server` - The test server instance
198
+ /// * `service` - Fully qualified service name (e.g., "mypackage.UserService")
199
+ /// * `method` - Method name (e.g., "GetUser")
200
+ /// * `payload` - Serialized protobuf message bytes
201
+ /// * `metadata` - gRPC metadata (headers) to include in the request
202
+ ///
203
+ /// # Errors
204
+ ///
205
+ /// Returns an error if:
206
+ /// - The service is not registered on the server
207
+ /// - The handler returns an error response
208
+ ///
209
+ /// # Example
210
+ ///
211
+ /// ```ignore
212
+ /// let mut server = GrpcTestServer::new();
213
+ /// server.register_service(Arc::new(my_handler));
214
+ ///
215
+ /// let response = send_unary_request(
216
+ /// &server,
217
+ /// "mypackage.UserService",
218
+ /// "GetUser",
219
+ /// Bytes::from("request"),
220
+ /// create_test_metadata(),
221
+ /// ).await?;
222
+ /// ```
223
+ pub async fn send_unary_request(
224
+ server: &GrpcTestServer,
225
+ service: &str,
226
+ method: &str,
227
+ payload: Bytes,
228
+ metadata: MetadataMap,
229
+ ) -> Result<GrpcResponseData, Box<dyn std::error::Error>> {
230
+ let handler = server
231
+ .get_handler(service)
232
+ .ok_or_else(|| Box::new(std::io::Error::new(
233
+ std::io::ErrorKind::NotFound,
234
+ format!("Service not found: {}", service),
235
+ )) as Box<dyn std::error::Error>)?;
236
+
237
+ let request = GrpcRequestData {
238
+ service_name: service.to_string(),
239
+ method_name: method.to_string(),
240
+ payload,
241
+ metadata,
242
+ };
243
+
244
+ handler.call(request).await.map_err(|e| e.message().into())
245
+ }
246
+
247
+ /// Assert that a gRPC response payload matches the expected JSON value
248
+ ///
249
+ /// Parses the response payload as JSON and compares it with the expected value.
250
+ /// This is useful for testing JSON-based gRPC messages.
251
+ ///
252
+ /// # Panics
253
+ ///
254
+ /// Panics if the payload cannot be parsed as JSON or doesn't match the expected value.
255
+ ///
256
+ /// # Arguments
257
+ ///
258
+ /// * `response` - The gRPC response data
259
+ /// * `expected` - The expected JSON value
260
+ ///
261
+ /// # Example
262
+ ///
263
+ /// ```ignore
264
+ /// let response = send_unary_request(...).await?;
265
+ /// assert_grpc_response(response, json!({"id": 1, "name": "Alice"}));
266
+ /// ```
267
+ pub fn assert_grpc_response(response: GrpcResponseData, expected: Value) {
268
+ let actual = serde_json::from_slice::<Value>(&response.payload)
269
+ .expect("Failed to parse response payload as JSON");
270
+
271
+ assert_eq!(
272
+ actual, expected,
273
+ "Response payload mismatch.\nExpected: {}\nActual: {}",
274
+ expected, actual
275
+ );
276
+ }
277
+
278
+ /// Assert that a gRPC handler result has a specific status code
279
+ ///
280
+ /// Useful for testing error handling and status code responses.
281
+ ///
282
+ /// # Panics
283
+ ///
284
+ /// Panics if the status code doesn't match.
285
+ ///
286
+ /// # Arguments
287
+ ///
288
+ /// * `result` - The gRPC handler result
289
+ /// * `expected_status` - The expected tonic::Code
290
+ ///
291
+ /// # Example
292
+ ///
293
+ /// ```ignore
294
+ /// let result = handler.call(request).await;
295
+ /// assert_grpc_status(&result, tonic::Code::NotFound);
296
+ /// ```
297
+ pub fn assert_grpc_status(result: &GrpcHandlerResult, expected_status: Code) {
298
+ match result {
299
+ Err(status) => {
300
+ assert_eq!(
301
+ status.code(),
302
+ expected_status,
303
+ "Expected status {:?} but got {:?}: {}",
304
+ expected_status,
305
+ status.code(),
306
+ status.message()
307
+ );
308
+ }
309
+ Ok(_) => {
310
+ panic!(
311
+ "Expected error status {:?} but got success response",
312
+ expected_status
313
+ );
314
+ }
315
+ }
316
+ }
317
+
318
+ /// Fluent builder for creating test protobuf messages
319
+ ///
320
+ /// Provides a simple way to construct JSON-serializable test messages
321
+ /// that can be used as gRPC payloads. In practice, you would use actual
322
+ /// protobuf code generation, but this is useful for simple tests.
323
+ ///
324
+ /// # Example
325
+ ///
326
+ /// ```ignore
327
+ /// let message = ProtobufMessageBuilder::new()
328
+ /// .add_field("id", 42)
329
+ /// .add_field("name", "Alice")
330
+ /// .add_field("email", "alice@example.com")
331
+ /// .build()?;
332
+ /// ```
333
+ pub struct ProtobufMessageBuilder {
334
+ fields: serde_json::Map<String, Value>,
335
+ }
336
+
337
+ impl ProtobufMessageBuilder {
338
+ /// Create a new message builder with no fields
339
+ pub fn new() -> Self {
340
+ Self {
341
+ fields: serde_json::Map::new(),
342
+ }
343
+ }
344
+
345
+ /// Add a field to the message
346
+ ///
347
+ /// # Arguments
348
+ ///
349
+ /// * `name` - The field name
350
+ /// * `value` - The field value (automatically converted to JSON)
351
+ ///
352
+ /// # Example
353
+ ///
354
+ /// ```ignore
355
+ /// builder.add_field("name", "Alice")
356
+ /// .add_field("age", 30)
357
+ /// .add_field("active", true);
358
+ /// ```
359
+ pub fn add_field(&mut self, name: &str, value: impl Into<Value>) -> &mut Self {
360
+ self.fields.insert(name.to_string(), value.into());
361
+ self
362
+ }
363
+
364
+ /// Add a string field to the message
365
+ pub fn add_string_field(&mut self, name: &str, value: &str) -> &mut Self {
366
+ self.fields
367
+ .insert(name.to_string(), Value::String(value.to_string()));
368
+ self
369
+ }
370
+
371
+ /// Add an integer field to the message
372
+ pub fn add_int_field(&mut self, name: &str, value: i64) -> &mut Self {
373
+ self.fields
374
+ .insert(name.to_string(), json!(value));
375
+ self
376
+ }
377
+
378
+ /// Add a boolean field to the message
379
+ pub fn add_bool_field(&mut self, name: &str, value: bool) -> &mut Self {
380
+ self.fields.insert(name.to_string(), json!(value));
381
+ self
382
+ }
383
+
384
+ /// Add a nested object field to the message
385
+ pub fn add_object_field(&mut self, name: &str, value: Value) -> &mut Self {
386
+ self.fields.insert(name.to_string(), value);
387
+ self
388
+ }
389
+
390
+ /// Add an array field to the message
391
+ pub fn add_array_field(&mut self, name: &str, values: Vec<Value>) -> &mut Self {
392
+ self.fields.insert(name.to_string(), Value::Array(values));
393
+ self
394
+ }
395
+
396
+ /// Build the message into serialized bytes
397
+ ///
398
+ /// # Errors
399
+ ///
400
+ /// Returns an error if the message cannot be serialized to JSON.
401
+ pub fn build(&self) -> Result<Bytes, Box<dyn std::error::Error>> {
402
+ let json_value = Value::Object(self.fields.clone());
403
+ let serialized = serde_json::to_vec(&json_value)?;
404
+ Ok(Bytes::from(serialized))
405
+ }
406
+
407
+ /// Clear all fields from the message
408
+ pub fn clear(&mut self) -> &mut Self {
409
+ self.fields.clear();
410
+ self
411
+ }
412
+
413
+ /// Get the current fields as a JSON object
414
+ pub fn as_json(&self) -> Value {
415
+ Value::Object(self.fields.clone())
416
+ }
417
+
418
+ /// Get the number of fields in the message
419
+ pub fn field_count(&self) -> usize {
420
+ self.fields.len()
421
+ }
422
+ }
423
+
424
+ impl Default for ProtobufMessageBuilder {
425
+ fn default() -> Self {
426
+ Self::new()
427
+ }
428
+ }
429
+
430
+ /// Create test metadata with common default headers
431
+ ///
432
+ /// Useful for constructing standard gRPC request metadata for testing.
433
+ ///
434
+ /// # Example
435
+ ///
436
+ /// ```ignore
437
+ /// let metadata = create_test_metadata();
438
+ /// // metadata now contains typical gRPC headers
439
+ /// ```
440
+ pub fn create_test_metadata() -> MetadataMap {
441
+ let mut metadata = MetadataMap::new();
442
+ metadata.insert("user-agent", "spikard-test/1.0".parse().unwrap());
443
+ metadata.insert("content-type", "application/grpc".parse().unwrap());
444
+ metadata
445
+ }
446
+
447
+ /// Create test metadata with custom headers
448
+ ///
449
+ /// Allows building metadata from a HashMap of key-value pairs.
450
+ ///
451
+ /// # Arguments
452
+ ///
453
+ /// * `headers` - HashMap of header names to values (String-based)
454
+ ///
455
+ /// # Example
456
+ ///
457
+ /// ```ignore
458
+ /// use std::collections::HashMap;
459
+ ///
460
+ /// let mut headers = HashMap::new();
461
+ /// headers.insert("authorization".to_string(), "Bearer token123".to_string());
462
+ /// headers.insert("x-custom".to_string(), "value".to_string());
463
+ ///
464
+ /// let metadata = create_test_metadata_with_headers(&headers).unwrap();
465
+ /// ```
466
+ pub fn create_test_metadata_with_headers(
467
+ headers: &std::collections::HashMap<String, String>,
468
+ ) -> Result<MetadataMap, Box<dyn std::error::Error>> {
469
+ use std::str::FromStr;
470
+ let mut metadata = MetadataMap::new();
471
+ for (key, value) in headers {
472
+ let meta_key = tonic::metadata::MetadataKey::from_str(key)?;
473
+ metadata.insert(meta_key, value.parse()?);
474
+ }
475
+ Ok(metadata)
476
+ }
477
+
478
+ /// Add authentication metadata to an existing MetadataMap
479
+ ///
480
+ /// Adds a standard Bearer token authorization header.
481
+ ///
482
+ /// # Arguments
483
+ ///
484
+ /// * `metadata` - The metadata map to modify
485
+ /// * `token` - The authentication token
486
+ ///
487
+ /// # Example
488
+ ///
489
+ /// ```ignore
490
+ /// let mut metadata = create_test_metadata();
491
+ /// add_auth_metadata(&mut metadata, "secret_token_123");
492
+ /// ```
493
+ pub fn add_auth_metadata(metadata: &mut MetadataMap, token: &str) -> Result<(), Box<dyn std::error::Error>> {
494
+ let auth_value = format!("Bearer {}", token);
495
+ metadata.insert("authorization", auth_value.parse()?);
496
+ Ok(())
497
+ }
498
+
499
+ /// Add custom metadata header to an existing MetadataMap
500
+ ///
501
+ /// # Arguments
502
+ ///
503
+ /// * `metadata` - The metadata map to modify
504
+ /// * `key` - The header name
505
+ /// * `value` - The header value
506
+ ///
507
+ /// # Example
508
+ ///
509
+ /// ```ignore
510
+ /// let mut metadata = create_test_metadata();
511
+ /// add_metadata_header(&mut metadata, "x-request-id", "req-123")?;
512
+ /// ```
513
+ pub fn add_metadata_header(
514
+ metadata: &mut MetadataMap,
515
+ key: &str,
516
+ value: &str,
517
+ ) -> Result<(), Box<dyn std::error::Error>> {
518
+ use std::str::FromStr;
519
+ let meta_key = tonic::metadata::MetadataKey::from_str(key)?;
520
+ metadata.insert(meta_key, value.parse()?);
521
+ Ok(())
522
+ }
523
+
524
+ /// Mock gRPC handler for testing
525
+ ///
526
+ /// A simple mock handler that always returns a fixed response. Useful for
527
+ /// basic integration tests that just need a handler to exist.
528
+ pub struct MockGrpcHandler {
529
+ service_name: &'static str,
530
+ response_payload: Bytes,
531
+ }
532
+
533
+ impl MockGrpcHandler {
534
+ /// Create a new mock handler
535
+ ///
536
+ /// # Arguments
537
+ ///
538
+ /// * `service_name` - The service name this handler serves (must be a static string)
539
+ /// * `response_payload` - The payload to return in responses
540
+ pub fn new(service_name: &'static str, response_payload: impl Into<Bytes>) -> Self {
541
+ Self {
542
+ service_name,
543
+ response_payload: response_payload.into(),
544
+ }
545
+ }
546
+
547
+ /// Create a mock handler that returns JSON
548
+ pub fn with_json(service_name: &'static str, json: Value) -> Self {
549
+ let bytes = serde_json::to_vec(&json).unwrap_or_default();
550
+ Self::new(service_name, Bytes::from(bytes))
551
+ }
552
+ }
553
+
554
+ impl GrpcHandler for MockGrpcHandler {
555
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
556
+ let response = GrpcResponseData {
557
+ payload: self.response_payload.clone(),
558
+ metadata: MetadataMap::new(),
559
+ };
560
+ Box::pin(async move { Ok(response) })
561
+ }
562
+
563
+ fn service_name(&self) -> &'static str {
564
+ self.service_name
565
+ }
566
+ }
567
+
568
+ /// Error mock handler for testing error responses
569
+ ///
570
+ /// A mock handler that always returns an error. Useful for testing error handling.
571
+ pub struct ErrorMockHandler {
572
+ service_name: String,
573
+ error_code: Code,
574
+ error_message: String,
575
+ }
576
+
577
+ impl ErrorMockHandler {
578
+ /// Create a new error mock handler
579
+ ///
580
+ /// # Arguments
581
+ ///
582
+ /// * `service_name` - The service name this handler serves
583
+ /// * `error_code` - The gRPC error code to return
584
+ /// * `error_message` - The error message
585
+ pub fn new(service_name: impl Into<String>, error_code: Code, error_message: impl Into<String>) -> Self {
586
+ Self {
587
+ service_name: service_name.into(),
588
+ error_code,
589
+ error_message: error_message.into(),
590
+ }
591
+ }
592
+ }
593
+
594
+ impl GrpcHandler for ErrorMockHandler {
595
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
596
+ let message = self.error_message.clone();
597
+ let code = self.error_code;
598
+ Box::pin(async move { Err(tonic::Status::new(code, message)) })
599
+ }
600
+
601
+ fn service_name(&self) -> &'static str {
602
+ "mock.ErrorMockService"
603
+ }
604
+ }
605
+
606
+ /// Echo mock handler for testing request/response flow
607
+ ///
608
+ /// A mock handler that echoes the request payload back as the response.
609
+ pub struct EchoMockHandler {
610
+ service_name: String,
611
+ }
612
+
613
+ impl EchoMockHandler {
614
+ /// Create a new echo mock handler
615
+ ///
616
+ /// # Arguments
617
+ ///
618
+ /// * `service_name` - The service name this handler serves
619
+ pub fn new(service_name: impl Into<String>) -> Self {
620
+ Self {
621
+ service_name: service_name.into(),
622
+ }
623
+ }
624
+ }
625
+
626
+ impl GrpcHandler for EchoMockHandler {
627
+ fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
628
+ let payload = request.payload;
629
+ Box::pin(async move {
630
+ Ok(GrpcResponseData {
631
+ payload,
632
+ metadata: MetadataMap::new(),
633
+ })
634
+ })
635
+ }
636
+
637
+ fn service_name(&self) -> &'static str {
638
+ "mock.EchoMockService"
639
+ }
640
+ }
641
+
642
+ #[cfg(test)]
643
+ mod tests {
644
+ use super::*;
645
+
646
+ #[test]
647
+ fn test_grpc_test_server_new() {
648
+ let server = GrpcTestServer::new();
649
+ assert_eq!(server.url(), "http://localhost:50051");
650
+ assert_eq!(server.service_count(), 0);
651
+ assert!(server.handlers().is_empty());
652
+ }
653
+
654
+ #[test]
655
+ fn test_grpc_test_server_with_url() {
656
+ let server = GrpcTestServer::with_url("http://localhost:8080");
657
+ assert_eq!(server.url(), "http://localhost:8080");
658
+ }
659
+
660
+ #[test]
661
+ fn test_grpc_test_server_register_service() {
662
+ let mut server = GrpcTestServer::new();
663
+ let handler = Arc::new(MockGrpcHandler::new("test.Service", "response"));
664
+
665
+ server.register_service(handler);
666
+
667
+ assert_eq!(server.service_count(), 1);
668
+ assert!(!server.handlers().is_empty());
669
+ }
670
+
671
+ #[test]
672
+ fn test_grpc_test_server_register_multiple_services() {
673
+ let mut server = GrpcTestServer::new();
674
+ let handler1 = Arc::new(MockGrpcHandler::new("service1", "response1"));
675
+ let handler2 = Arc::new(MockGrpcHandler::new("service2", "response2"));
676
+
677
+ server.register_service(handler1);
678
+ server.register_service(handler2);
679
+
680
+ assert_eq!(server.service_count(), 2);
681
+ }
682
+
683
+ #[test]
684
+ fn test_grpc_test_server_default() {
685
+ let server = GrpcTestServer::default();
686
+ assert_eq!(server.service_count(), 0);
687
+ }
688
+
689
+ #[tokio::test]
690
+ async fn test_mock_grpc_handler_basic() {
691
+ let handler = MockGrpcHandler::new("test.Service", Bytes::from("response"));
692
+ let request = GrpcRequestData {
693
+ service_name: "test.Service".to_string(),
694
+ method_name: "TestMethod".to_string(),
695
+ payload: Bytes::from("request"),
696
+ metadata: MetadataMap::new(),
697
+ };
698
+
699
+ let result = handler.call(request).await;
700
+ assert!(result.is_ok());
701
+
702
+ let response = result.unwrap();
703
+ assert_eq!(response.payload, Bytes::from("response"));
704
+ }
705
+
706
+ #[tokio::test]
707
+ async fn test_mock_grpc_handler_with_json() {
708
+ let json_response = json!({"id": 1, "name": "Alice"});
709
+ let handler = MockGrpcHandler::with_json("test.UserService", json_response.clone());
710
+
711
+ let request = GrpcRequestData {
712
+ service_name: "test.UserService".to_string(),
713
+ method_name: "GetUser".to_string(),
714
+ payload: Bytes::from("{}"),
715
+ metadata: MetadataMap::new(),
716
+ };
717
+
718
+ let result = handler.call(request).await;
719
+ assert!(result.is_ok());
720
+
721
+ let response = result.unwrap();
722
+ let deserialized = serde_json::from_slice::<Value>(&response.payload).unwrap();
723
+ assert_eq!(deserialized, json_response);
724
+ }
725
+
726
+ #[tokio::test]
727
+ async fn test_error_mock_handler() {
728
+ let handler = ErrorMockHandler::new("test.Service", Code::NotFound, "Resource not found");
729
+
730
+ let request = GrpcRequestData {
731
+ service_name: "test.Service".to_string(),
732
+ method_name: "GetResource".to_string(),
733
+ payload: Bytes::new(),
734
+ metadata: MetadataMap::new(),
735
+ };
736
+
737
+ let result = handler.call(request).await;
738
+ assert!(result.is_err());
739
+
740
+ let status = result.unwrap_err();
741
+ assert_eq!(status.code(), Code::NotFound);
742
+ assert_eq!(status.message(), "Resource not found");
743
+ }
744
+
745
+ #[tokio::test]
746
+ async fn test_echo_mock_handler() {
747
+ let handler = EchoMockHandler::new("test.Service");
748
+ let payload = Bytes::from("echo this");
749
+
750
+ let request = GrpcRequestData {
751
+ service_name: "test.Service".to_string(),
752
+ method_name: "Echo".to_string(),
753
+ payload: payload.clone(),
754
+ metadata: MetadataMap::new(),
755
+ };
756
+
757
+ let result = handler.call(request).await;
758
+ assert!(result.is_ok());
759
+
760
+ let response = result.unwrap();
761
+ assert_eq!(response.payload, payload);
762
+ }
763
+
764
+ #[test]
765
+ fn test_protobuf_message_builder_new() {
766
+ let builder = ProtobufMessageBuilder::new();
767
+ assert_eq!(builder.field_count(), 0);
768
+ }
769
+
770
+ #[test]
771
+ fn test_protobuf_message_builder_add_field() {
772
+ let mut builder = ProtobufMessageBuilder::new();
773
+ builder.add_field("name", "Alice").add_field("age", 30);
774
+
775
+ assert_eq!(builder.field_count(), 2);
776
+ }
777
+
778
+ #[test]
779
+ fn test_protobuf_message_builder_add_string_field() {
780
+ let mut builder = ProtobufMessageBuilder::new();
781
+ builder.add_string_field("name", "Bob");
782
+
783
+ let json = builder.as_json();
784
+ assert_eq!(json["name"], "Bob");
785
+ }
786
+
787
+ #[test]
788
+ fn test_protobuf_message_builder_add_int_field() {
789
+ let mut builder = ProtobufMessageBuilder::new();
790
+ builder.add_int_field("age", 42);
791
+
792
+ let json = builder.as_json();
793
+ assert_eq!(json["age"], 42);
794
+ }
795
+
796
+ #[test]
797
+ fn test_protobuf_message_builder_add_bool_field() {
798
+ let mut builder = ProtobufMessageBuilder::new();
799
+ builder.add_bool_field("active", true);
800
+
801
+ let json = builder.as_json();
802
+ assert_eq!(json["active"], true);
803
+ }
804
+
805
+ #[test]
806
+ fn test_protobuf_message_builder_add_object_field() {
807
+ let mut builder = ProtobufMessageBuilder::new();
808
+ builder.add_object_field("user", json!({"name": "Alice", "id": 1}));
809
+
810
+ let json = builder.as_json();
811
+ assert_eq!(json["user"]["name"], "Alice");
812
+ assert_eq!(json["user"]["id"], 1);
813
+ }
814
+
815
+ #[test]
816
+ fn test_protobuf_message_builder_add_array_field() {
817
+ let mut builder = ProtobufMessageBuilder::new();
818
+ builder.add_array_field("tags", vec![json!("rust"), json!("testing"), json!("grpc")]);
819
+
820
+ let json = builder.as_json();
821
+ assert_eq!(json["tags"].as_array().unwrap().len(), 3);
822
+ assert_eq!(json["tags"][0], "rust");
823
+ }
824
+
825
+ #[test]
826
+ fn test_protobuf_message_builder_build() {
827
+ let mut builder = ProtobufMessageBuilder::new();
828
+ builder.add_field("id", 1).add_field("name", "Alice");
829
+
830
+ let bytes = builder.build().unwrap();
831
+ let deserialized = serde_json::from_slice::<Value>(&bytes).unwrap();
832
+
833
+ assert_eq!(deserialized["id"], 1);
834
+ assert_eq!(deserialized["name"], "Alice");
835
+ }
836
+
837
+ #[test]
838
+ fn test_protobuf_message_builder_clear() {
839
+ let mut builder = ProtobufMessageBuilder::new();
840
+ builder.add_field("id", 1).add_field("name", "Alice");
841
+ assert_eq!(builder.field_count(), 2);
842
+
843
+ builder.clear();
844
+ assert_eq!(builder.field_count(), 0);
845
+ }
846
+
847
+ #[test]
848
+ fn test_protobuf_message_builder_fluent_api() {
849
+ let mut builder = ProtobufMessageBuilder::new();
850
+ builder
851
+ .add_string_field("email", "alice@example.com")
852
+ .add_int_field("age", 30)
853
+ .add_bool_field("verified", true);
854
+
855
+ assert_eq!(builder.field_count(), 3);
856
+
857
+ let json = builder.as_json();
858
+ assert_eq!(json["email"], "alice@example.com");
859
+ assert_eq!(json["age"], 30);
860
+ assert_eq!(json["verified"], true);
861
+ }
862
+
863
+ #[test]
864
+ fn test_create_test_metadata() {
865
+ let metadata = create_test_metadata();
866
+
867
+ assert!(metadata.get("user-agent").is_some());
868
+ assert!(metadata.get("content-type").is_some());
869
+ }
870
+
871
+ #[test]
872
+ fn test_create_test_metadata_with_headers() {
873
+ use std::collections::HashMap;
874
+
875
+ let mut headers = HashMap::new();
876
+ headers.insert("authorization".to_string(), "Bearer token".to_string());
877
+ headers.insert("x-custom".to_string(), "value".to_string());
878
+
879
+ let metadata = create_test_metadata_with_headers(&headers).unwrap();
880
+
881
+ assert!(metadata.get("authorization").is_some());
882
+ assert!(metadata.get("x-custom").is_some());
883
+ }
884
+
885
+ #[test]
886
+ fn test_add_auth_metadata() {
887
+ let mut metadata = create_test_metadata();
888
+ add_auth_metadata(&mut metadata, "secret_token_123").unwrap();
889
+
890
+ let auth_header = metadata.get("authorization").unwrap();
891
+ assert_eq!(auth_header.to_str().unwrap(), "Bearer secret_token_123");
892
+ }
893
+
894
+ #[test]
895
+ fn test_add_metadata_header() {
896
+ let mut metadata = create_test_metadata();
897
+ add_metadata_header(&mut metadata, "x-request-id", "req-123").unwrap();
898
+
899
+ let header = metadata.get("x-request-id").unwrap();
900
+ assert_eq!(header.to_str().unwrap(), "req-123");
901
+ }
902
+
903
+ #[test]
904
+ fn test_assert_grpc_response_matching() {
905
+ let response = GrpcResponseData {
906
+ payload: Bytes::from(r#"{"id": 1, "name": "Alice"}"#),
907
+ metadata: MetadataMap::new(),
908
+ };
909
+
910
+ let expected = json!({"id": 1, "name": "Alice"});
911
+ assert_grpc_response(response, expected);
912
+ }
913
+
914
+ #[test]
915
+ fn test_assert_grpc_status_error() {
916
+ let result: GrpcHandlerResult = Err(tonic::Status::not_found("Resource not found"));
917
+
918
+ assert_grpc_status(&result, Code::NotFound);
919
+ }
920
+
921
+ #[test]
922
+ #[should_panic(expected = "Expected status")]
923
+ fn test_assert_grpc_status_mismatch() {
924
+ let result: GrpcHandlerResult = Err(tonic::Status::not_found("Resource not found"));
925
+
926
+ assert_grpc_status(&result, Code::InvalidArgument);
927
+ }
928
+
929
+ #[test]
930
+ #[should_panic(expected = "Expected error status")]
931
+ fn test_assert_grpc_status_on_success() {
932
+ let result: GrpcHandlerResult = Ok(GrpcResponseData {
933
+ payload: Bytes::new(),
934
+ metadata: MetadataMap::new(),
935
+ });
936
+
937
+ assert_grpc_status(&result, Code::NotFound);
938
+ }
939
+
940
+ #[test]
941
+ fn test_protobuf_message_builder_default() {
942
+ let builder = ProtobufMessageBuilder::default();
943
+ assert_eq!(builder.field_count(), 0);
944
+ }
945
+
946
+ #[tokio::test]
947
+ async fn test_send_unary_request_with_mock_handler() {
948
+ let mut server = GrpcTestServer::new();
949
+ let _response_payload = json!({"result": "success"});
950
+
951
+ // Create a custom handler that reports the correct service name
952
+ struct TestHandler;
953
+ impl GrpcHandler for TestHandler {
954
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
955
+ Box::pin(async {
956
+ Ok(GrpcResponseData {
957
+ payload: serde_json::to_vec(&json!({"result": "success"})).unwrap().into(),
958
+ metadata: MetadataMap::new(),
959
+ })
960
+ })
961
+ }
962
+ fn service_name(&self) -> &'static str {
963
+ "test.TestService"
964
+ }
965
+ }
966
+
967
+ server.register_service(Arc::new(TestHandler));
968
+
969
+ let message = ProtobufMessageBuilder::new()
970
+ .add_field("input", "test")
971
+ .build()
972
+ .unwrap();
973
+
974
+ let result = send_unary_request(
975
+ &server,
976
+ "test.TestService",
977
+ "TestMethod",
978
+ message,
979
+ create_test_metadata(),
980
+ )
981
+ .await;
982
+
983
+ assert!(result.is_ok());
984
+ }
985
+
986
+ #[tokio::test]
987
+ async fn test_send_unary_request_service_not_found() {
988
+ let server = GrpcTestServer::new();
989
+ let message = Bytes::from("test");
990
+
991
+ let result = send_unary_request(
992
+ &server,
993
+ "nonexistent.Service",
994
+ "Method",
995
+ message,
996
+ create_test_metadata(),
997
+ )
998
+ .await;
999
+
1000
+ assert!(result.is_err());
1001
+ }
1002
+
1003
+ #[test]
1004
+ fn test_error_mock_handler_invalid_argument() {
1005
+ let handler = ErrorMockHandler::new("test.Service", Code::InvalidArgument, "Bad input");
1006
+ assert_eq!(handler.error_code, Code::InvalidArgument);
1007
+ assert_eq!(handler.error_message, "Bad input");
1008
+ }
1009
+
1010
+ #[test]
1011
+ fn test_echo_mock_handler_creates_with_service_name() {
1012
+ let handler = EchoMockHandler::new("mypackage.EchoService");
1013
+ assert_eq!(handler.service_name, "mypackage.EchoService");
1014
+ }
1015
+
1016
+ #[test]
1017
+ fn test_grpc_test_client_creation() {
1018
+ let _client = create_grpc_test_client();
1019
+ let _default_client = GrpcTestClient::default();
1020
+ let _new_client = GrpcTestClient::new();
1021
+ // Just ensure it can be created without panicking
1022
+ }
1023
+ }