itsi 0.1.20 → 0.2.2

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 (319) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -8
  3. data/Cargo.lock +2 -2
  4. data/LICENSE.txt +698 -0
  5. data/README.md +15 -4
  6. data/Rakefile +9 -5
  7. data/crates/itsi_acme/.gitignore +4 -0
  8. data/crates/itsi_acme/Cargo.toml +86 -0
  9. data/crates/itsi_acme/LICENSE-APACHE +201 -0
  10. data/crates/itsi_acme/LICENSE-MIT +23 -0
  11. data/crates/itsi_acme/README.md +9 -0
  12. data/crates/itsi_acme/examples/high_level.rs +63 -0
  13. data/crates/itsi_acme/examples/high_level_warp.rs +52 -0
  14. data/crates/itsi_acme/examples/low_level.rs +87 -0
  15. data/crates/itsi_acme/examples/low_level_axum.rs +66 -0
  16. data/crates/itsi_acme/src/acceptor.rs +81 -0
  17. data/crates/itsi_acme/src/acme.rs +354 -0
  18. data/crates/itsi_acme/src/axum.rs +86 -0
  19. data/crates/itsi_acme/src/cache.rs +39 -0
  20. data/crates/itsi_acme/src/caches/boxed.rs +80 -0
  21. data/crates/itsi_acme/src/caches/composite.rs +69 -0
  22. data/crates/itsi_acme/src/caches/dir.rs +106 -0
  23. data/crates/itsi_acme/src/caches/mod.rs +11 -0
  24. data/crates/itsi_acme/src/caches/no.rs +78 -0
  25. data/crates/itsi_acme/src/caches/test.rs +136 -0
  26. data/crates/itsi_acme/src/config.rs +172 -0
  27. data/crates/itsi_acme/src/https_helper.rs +69 -0
  28. data/crates/itsi_acme/src/incoming.rs +142 -0
  29. data/crates/itsi_acme/src/jose.rs +161 -0
  30. data/crates/itsi_acme/src/lib.rs +142 -0
  31. data/crates/itsi_acme/src/resolver.rs +59 -0
  32. data/crates/itsi_acme/src/state.rs +424 -0
  33. data/crates/itsi_rb_helpers/src/lib.rs +4 -3
  34. data/crates/itsi_scheduler/Cargo.toml +1 -1
  35. data/crates/itsi_scheduler/src/itsi_scheduler.rs +8 -2
  36. data/crates/itsi_scheduler/src/lib.rs +1 -0
  37. data/crates/itsi_server/Cargo.toml +1 -1
  38. data/crates/itsi_server/src/lib.rs +2 -1
  39. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +18 -1
  40. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +11 -3
  41. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +122 -63
  42. data/crates/itsi_server/src/ruby_types/itsi_server.rs +2 -0
  43. data/crates/itsi_server/src/server/binds/bind.rs +3 -0
  44. data/crates/itsi_server/src/server/binds/listener.rs +12 -5
  45. data/crates/itsi_server/src/server/binds/tls.rs +13 -5
  46. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +12 -5
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +8 -1
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +9 -1
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +48 -43
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +11 -2
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +39 -12
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +36 -27
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/csp.rs +25 -11
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +12 -3
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +74 -72
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +15 -1
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +11 -8
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +19 -11
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +5 -5
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +2 -2
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +11 -5
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +17 -20
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +19 -8
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +16 -37
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +22 -12
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +26 -11
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +7 -1
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +14 -4
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +19 -0
  70. data/crates/itsi_server/src/server/middleware_stack/mod.rs +49 -13
  71. data/crates/itsi_server/src/server/mod.rs +1 -0
  72. data/crates/itsi_server/src/server/redirect_type.rs +26 -0
  73. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +22 -16
  74. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +49 -12
  75. data/crates/itsi_server/src/server/signal.rs +1 -0
  76. data/crates/itsi_server/src/server/size_limited_incoming.rs +6 -0
  77. data/crates/itsi_server/src/server/thread_worker.rs +5 -1
  78. data/crates/itsi_server/src/services/itsi_http_service.rs +20 -2
  79. data/crates/itsi_server/src/services/rate_limiter.rs +15 -4
  80. data/crates/itsi_server/src/services/static_file_server.rs +33 -19
  81. data/crates/itsi_tracing/src/lib.rs +42 -22
  82. data/docs/content/_index.md +1 -2
  83. data/docs/content/acknowledgements/_index.md +5 -2
  84. data/docs/content/configuration/_index.md +8 -5
  85. data/docs/content/contact/_index.md +8 -1
  86. data/docs/content/faqs/_index.md +5 -3
  87. data/docs/content/features/_index.md +56 -50
  88. data/docs/content/getting_started/_index.md +8 -5
  89. data/docs/content/getting_started/local_development.md +68 -8
  90. data/docs/content/getting_started/logging.md +16 -9
  91. data/docs/content/getting_started/running_itsi_in_production.md +5 -3
  92. data/docs/content/getting_started/signals.md +38 -0
  93. data/docs/content/itsi_scheduler/_index.md +8 -7
  94. data/docs/content/utilities/_index.md +13 -0
  95. data/docs/content/utilities/config_file_testing.md +17 -0
  96. data/docs/content/utilities/passfile_generator.md +41 -0
  97. data/docs/content/utilities/route_testing.md +27 -0
  98. data/docs/content/utilities/secrets_management.md +30 -0
  99. data/docs/hugo.yaml +1 -1
  100. data/fairytale.txt +3 -4
  101. data/gems/scheduler/Cargo.lock +1 -1
  102. data/gems/scheduler/README.md +4 -5
  103. data/gems/scheduler/Rakefile +0 -4
  104. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  105. data/gems/scheduler/lib/itsi/scheduler.rb +9 -4
  106. data/gems/scheduler/test/test_active_record.rb +12 -7
  107. data/gems/server/Cargo.lock +1 -1
  108. data/gems/server/Rakefile +0 -4
  109. data/gems/server/exe/itsi +13 -2
  110. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +2 -0
  111. data/gems/server/lib/itsi/http_request.rb +40 -9
  112. data/gems/server/lib/itsi/http_response.rb +2 -1
  113. data/gems/server/lib/itsi/passfile.rb +0 -1
  114. data/gems/server/lib/itsi/server/config/config_helpers.rb +20 -8
  115. data/gems/server/lib/itsi/server/config/dsl.rb +20 -435
  116. data/gems/server/lib/itsi/server/config/known_paths.rb +4 -1
  117. data/gems/server/lib/itsi/server/config/middleware/_index.md +6 -4
  118. data/gems/server/lib/itsi/server/config/middleware/allow_list.md +46 -0
  119. data/gems/server/lib/itsi/server/config/middleware/allow_list.rb +42 -0
  120. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.md +90 -0
  121. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.rb +51 -0
  122. data/gems/server/lib/itsi/server/config/middleware/auth_basic.md +45 -0
  123. data/gems/server/lib/itsi/server/config/middleware/auth_basic.rb +44 -0
  124. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.md +82 -0
  125. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.rb +38 -0
  126. data/gems/server/lib/itsi/server/config/middleware/cache_control.md +78 -0
  127. data/gems/server/lib/itsi/server/config/middleware/cache_control.rb +45 -0
  128. data/gems/server/lib/itsi/server/config/middleware/cidr_to_regex.rb +50 -0
  129. data/gems/server/lib/itsi/server/config/middleware/compression.md +50 -0
  130. data/gems/server/lib/itsi/server/config/middleware/compression.rb +37 -0
  131. data/gems/server/lib/itsi/server/config/middleware/cors.md +93 -0
  132. data/gems/server/lib/itsi/server/config/middleware/cors.rb +32 -0
  133. data/gems/server/lib/itsi/server/config/middleware/csp.md +37 -0
  134. data/gems/server/lib/itsi/server/config/middleware/csp.rb +44 -0
  135. data/gems/server/lib/itsi/server/config/middleware/deny_list.md +45 -0
  136. data/gems/server/lib/itsi/server/config/middleware/deny_list.rb +42 -0
  137. data/gems/server/lib/itsi/server/config/middleware/endpoint/_index.md +159 -0
  138. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.md +186 -0
  139. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.rb +33 -0
  140. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.md +12 -0
  141. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.rb +42 -0
  142. data/gems/server/lib/itsi/server/config/middleware/endpoint/endpoint.rb +99 -0
  143. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.md +12 -0
  144. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.rb +42 -0
  145. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_request.md +44 -0
  146. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_response.md +39 -0
  147. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.md +12 -0
  148. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.rb +42 -0
  149. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.md +12 -0
  150. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.rb +42 -0
  151. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.md +12 -0
  152. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.rb +42 -0
  153. data/gems/server/lib/itsi/server/config/middleware/endpoint/schemas.md +122 -0
  154. data/gems/server/lib/itsi/server/config/middleware/error_response.md +61 -0
  155. data/gems/server/lib/itsi/server/config/middleware/error_response.rb +36 -0
  156. data/gems/server/lib/itsi/server/config/middleware/etag.md +59 -0
  157. data/gems/server/lib/itsi/server/config/middleware/etag.rb +27 -0
  158. data/gems/server/lib/itsi/server/config/middleware/grpc.md +172 -0
  159. data/gems/server/lib/itsi/server/config/middleware/grpc.rb +54 -0
  160. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.md +124 -0
  161. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.rb +61 -0
  162. data/gems/server/lib/itsi/server/config/middleware/location.md +107 -0
  163. data/gems/server/lib/itsi/server/config/middleware/location.rb +99 -0
  164. data/gems/server/lib/itsi/server/config/middleware/log_requests.md +13 -11
  165. data/gems/server/lib/itsi/server/config/middleware/log_requests.rb +1 -3
  166. data/gems/server/lib/itsi/server/config/middleware/max_body.md +18 -0
  167. data/gems/server/lib/itsi/server/config/middleware/max_body.rb +21 -0
  168. data/gems/server/lib/itsi/server/config/middleware/proxy.md +62 -0
  169. data/gems/server/lib/itsi/server/config/middleware/proxy.rb +41 -0
  170. data/gems/server/lib/itsi/server/config/middleware/rackup_file.md +54 -0
  171. data/gems/server/lib/itsi/server/config/middleware/rackup_file.rb +44 -0
  172. data/gems/server/lib/itsi/server/config/middleware/rate_limit.md +126 -0
  173. data/gems/server/lib/itsi/server/config/middleware/rate_limit.rb +34 -0
  174. data/gems/server/lib/itsi/server/config/middleware/rate_limit_store.rb +25 -0
  175. data/gems/server/lib/itsi/server/config/middleware/redirect.md +55 -0
  176. data/gems/server/lib/itsi/server/config/middleware/redirect.rb +25 -0
  177. data/gems/server/lib/itsi/server/config/middleware/request_headers.md +34 -0
  178. data/gems/server/lib/itsi/server/config/middleware/request_headers.rb +24 -0
  179. data/gems/server/lib/itsi/server/config/middleware/response_headers.md +33 -0
  180. data/gems/server/lib/itsi/server/config/middleware/response_headers.rb +25 -0
  181. data/gems/server/lib/itsi/server/config/middleware/run.md +60 -0
  182. data/gems/server/lib/itsi/server/config/middleware/run.rb +43 -0
  183. data/gems/server/lib/itsi/server/config/middleware/static_assets.md +73 -0
  184. data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +87 -0
  185. data/gems/server/lib/itsi/server/config/middleware/static_response.md +44 -0
  186. data/gems/server/lib/itsi/server/config/middleware/static_response.rb +29 -0
  187. data/gems/server/lib/itsi/server/config/middleware/string_rewrite.md +67 -0
  188. data/gems/server/lib/itsi/server/config/middleware/token_source.rb +32 -0
  189. data/gems/server/lib/itsi/server/config/middleware.rb +4 -0
  190. data/gems/server/lib/itsi/server/config/option.rb +5 -0
  191. data/gems/server/lib/itsi/server/config/options/_index.md +3 -2
  192. data/gems/server/lib/itsi/server/config/options/auto_reload_config.md +13 -0
  193. data/gems/server/lib/itsi/server/config/options/auto_reload_config.rb +41 -0
  194. data/gems/server/lib/itsi/server/config/options/bind.md +71 -0
  195. data/gems/server/lib/itsi/server/config/options/bind.rb +26 -0
  196. data/gems/server/lib/itsi/server/config/options/certificates.md +65 -0
  197. data/gems/server/lib/itsi/server/config/options/daemonize.md +14 -0
  198. data/gems/server/lib/itsi/server/config/options/daemonize.rb +19 -0
  199. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.md +1 -2
  200. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.rb +6 -3
  201. data/gems/server/lib/itsi/server/config/options/header_read_timeout.md +17 -0
  202. data/gems/server/lib/itsi/server/config/options/header_read_timeout.rb +19 -0
  203. data/gems/server/lib/itsi/server/config/options/hooks/_index.md +11 -0
  204. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.md +13 -0
  205. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.rb +28 -0
  206. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.md +14 -0
  207. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.rb +28 -0
  208. data/gems/server/lib/itsi/server/config/options/hooks/after_start.md +12 -0
  209. data/gems/server/lib/itsi/server/config/options/hooks/after_start.rb +28 -0
  210. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.md +13 -0
  211. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.rb +28 -0
  212. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.md +12 -0
  213. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.rb +28 -0
  214. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.md +12 -0
  215. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.rb +28 -0
  216. data/gems/server/lib/itsi/server/config/options/include.md +20 -0
  217. data/gems/server/lib/itsi/server/config/options/include.rb +36 -0
  218. data/gems/server/lib/itsi/server/config/options/listen_backlog.md +11 -0
  219. data/gems/server/lib/itsi/server/config/options/listen_backlog.rb +19 -0
  220. data/gems/server/lib/itsi/server/config/options/log_format.md +18 -0
  221. data/gems/server/lib/itsi/server/config/options/log_format.rb +19 -0
  222. data/gems/server/lib/itsi/server/config/options/log_level.md +34 -0
  223. data/gems/server/lib/itsi/server/config/options/log_level.rb +20 -0
  224. data/gems/server/lib/itsi/server/config/options/log_target.md +38 -0
  225. data/gems/server/lib/itsi/server/config/options/log_target.rb +19 -0
  226. data/gems/server/lib/itsi/server/config/options/log_target_filters.md +17 -0
  227. data/gems/server/lib/itsi/server/config/options/log_target_filters.rb +19 -0
  228. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.md +27 -0
  229. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.rb +24 -0
  230. data/gems/server/lib/itsi/server/config/options/nodelay.md +16 -0
  231. data/gems/server/lib/itsi/server/config/options/nodelay.rb +19 -0
  232. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.md +19 -0
  233. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.rb +18 -0
  234. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.md +17 -0
  235. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.rb +19 -0
  236. data/gems/server/lib/itsi/server/config/options/preload.md +21 -0
  237. data/gems/server/lib/itsi/server/config/options/preload.rb +18 -0
  238. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.md +15 -0
  239. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.rb +19 -0
  240. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.md +21 -0
  241. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.rb +30 -0
  242. data/gems/server/lib/itsi/server/config/options/request_timeout.md +23 -0
  243. data/gems/server/lib/itsi/server/config/options/request_timeout.rb +19 -0
  244. data/gems/server/lib/itsi/server/config/options/reuse_address.md +16 -0
  245. data/gems/server/lib/itsi/server/config/options/reuse_address.rb +19 -0
  246. data/gems/server/lib/itsi/server/config/options/reuse_port.md +16 -0
  247. data/gems/server/lib/itsi/server/config/options/reuse_port.rb +19 -0
  248. data/gems/server/lib/itsi/server/config/options/scheduler_threads.md +34 -0
  249. data/gems/server/lib/itsi/server/config/options/scheduler_threads.rb +17 -0
  250. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.md +17 -0
  251. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.rb +19 -0
  252. data/gems/server/lib/itsi/server/config/options/stream_body.md +32 -0
  253. data/gems/server/lib/itsi/server/config/options/stream_body.rb +18 -0
  254. data/gems/server/lib/itsi/server/config/options/threads.md +7 -2
  255. data/gems/server/lib/itsi/server/config/options/threads.rb +1 -1
  256. data/gems/server/lib/itsi/server/config/options/watch.md +16 -0
  257. data/gems/server/lib/itsi/server/config/options/watch.rb +28 -0
  258. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.md +22 -0
  259. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.rb +18 -0
  260. data/gems/server/lib/itsi/server/config/options/workers.md +1 -2
  261. data/gems/server/lib/itsi/server/config/options/workers.rb +1 -1
  262. data/gems/server/lib/itsi/server/config/typed_struct.rb +59 -20
  263. data/gems/server/lib/itsi/server/config.rb +77 -48
  264. data/gems/server/lib/itsi/server/default_config/Itsi.rb +3 -3
  265. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +1 -1
  266. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +11 -4
  267. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +3 -3
  268. data/gems/server/lib/itsi/server/route_tester.rb +58 -8
  269. data/gems/server/lib/itsi/server/signal_trap.rb +1 -1
  270. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +14 -18
  271. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +5 -4
  272. data/gems/server/lib/itsi/server/typed_handlers.rb +12 -4
  273. data/gems/server/lib/itsi/server/version.rb +1 -1
  274. data/gems/server/lib/itsi/server.rb +98 -14
  275. data/gems/server/lib/ruby_lsp/itsi/addon.rb +20 -18
  276. data/gems/server/test/helpers/test_helper.rb +89 -29
  277. data/gems/server/test/middleware/allow_list.rb +128 -0
  278. data/gems/server/test/middleware/auth_api_key.rb +141 -0
  279. data/gems/server/test/middleware/auth_basic.rb +91 -0
  280. data/gems/server/test/middleware/auth_jwt.rb +214 -0
  281. data/gems/server/test/middleware/cache_control.rb +82 -0
  282. data/gems/server/test/middleware/cidr_to_regex.rb +46 -0
  283. data/gems/server/test/middleware/compression.rb +89 -0
  284. data/gems/server/test/middleware/cors.rb +113 -0
  285. data/gems/server/test/middleware/csp.rb +62 -0
  286. data/gems/server/test/middleware/deny_list.rb +131 -0
  287. data/gems/server/test/middleware/endpoint.rb +300 -0
  288. data/gems/server/test/middleware/etag.rb +75 -0
  289. data/gems/server/test/middleware/grpc/grpc.rb +158 -0
  290. data/gems/server/test/middleware/grpc/test_service.proto +32 -0
  291. data/gems/server/test/middleware/grpc/test_service_impl.rb +28 -0
  292. data/gems/server/test/middleware/grpc/test_service_pb.rb +18 -0
  293. data/gems/server/test/middleware/grpc/test_service_services_pb.rb +30 -0
  294. data/gems/server/test/middleware/header_interpolation.rb +35 -0
  295. data/gems/server/test/middleware/intrusion_protection.rb +259 -0
  296. data/gems/server/test/middleware/location.rb +220 -0
  297. data/gems/server/test/middleware/max_body.rb +20 -0
  298. data/gems/server/test/middleware/proxy.rb +415 -0
  299. data/gems/server/test/middleware/rate_limit.rb +211 -0
  300. data/gems/server/test/middleware/redirect.rb +85 -0
  301. data/gems/server/test/middleware/request_headers.rb +50 -0
  302. data/gems/server/test/middleware/response_headers.rb +50 -0
  303. data/gems/server/test/middleware/static_assets.rb +374 -0
  304. data/gems/server/test/middleware/static_response.rb +56 -0
  305. data/gems/server/test/middleware/string_rewrite.rb +112 -0
  306. data/gems/server/test/options/bind.rb +47 -0
  307. data/gems/server/test/options/header_read_timeout.rb +23 -0
  308. data/gems/server/test/options/test_request_timeout.rb +16 -0
  309. data/gems/server/test/options/test_workers.rb +2 -4
  310. data/gems/server/test/{test_itsi_server.rb → rack/test_rack_server.rb} +2 -2
  311. data/grpc_test/Itsi.rb +11 -0
  312. data/grpc_test/echo.proto +14 -0
  313. data/grpc_test/echo_pb.rb +16 -0
  314. data/grpc_test/echo_service_impl.rb +8 -0
  315. data/grpc_test/echo_services_pb.rb +22 -0
  316. data/lib/itsi/version.rb +1 -1
  317. data/tasks.txt +15 -72
  318. metadata +210 -7
  319. data/gems/server/lib/itsi/server/default_config/Itsi-rackup.rb +0 -119
@@ -0,0 +1,131 @@
1
+ # test_deny_list.rb
2
+ require_relative "../helpers/test_helper"
3
+
4
+ class TestDenyList < Minitest::Test
5
+ # 1. Single‐pattern: localhost is denied
6
+ def test_single_pattern_denies_and_allows
7
+ server(
8
+ itsi_rb: lambda do
9
+ deny_list denied_patterns: ["^127\\.0\\.0\\.1$"]
10
+ get("/foo") { |r| r.ok "ok" }
11
+ end
12
+ ) do
13
+ # Client from 127.0.0.1 is denied
14
+ res1 = get_resp("/foo")
15
+ assert_equal "403", res1.code
16
+
17
+ # If we change the pattern to something else, localhost is allowed
18
+ server(
19
+ itsi_rb: lambda do
20
+ deny_list denied_patterns: ["^10\\."]
21
+ get("/foo") { |r| r.ok "allowed" }
22
+ end
23
+ ) do
24
+ res2 = get_resp("/foo")
25
+ assert_equal "200", res2.code
26
+ assert_equal "allowed", res2.body
27
+ end
28
+ end
29
+ end
30
+
31
+ # 2. Multiple‐pattern: deny localhost or 172.16.x.x
32
+ def test_multiple_patterns
33
+ server(
34
+ itsi_rb: lambda do
35
+ deny_list denied_patterns: ["^127\\.0\\.0\\.1$", "^172\\.16\\."]
36
+ get("/h") { |r| r.ok "h" }
37
+ end
38
+ ) do
39
+ # localhost matches first pattern → denied
40
+ res1 = get_resp("/h")
41
+ assert_equal "403", res1.code
42
+
43
+ # If we restrict only to 172.16.*, localhost becomes allowed
44
+ server(
45
+ itsi_rb: lambda do
46
+ deny_list denied_patterns: ["^172\\.16\\."]
47
+ get("/h") { |r| r.ok "ok" }
48
+ end
49
+ ) do
50
+ res2 = get_resp("/h")
51
+ assert_equal "200", res2.code
52
+ assert_equal "ok", res2.body
53
+ end
54
+ end
55
+ end
56
+
57
+ # 3. Custom error_response
58
+ def test_custom_error_response
59
+ server(
60
+ itsi_rb: lambda do
61
+ deny_list \
62
+ denied_patterns: ["^127\\.0\\.0\\.1$"],
63
+ error_response: {
64
+ code: 403,
65
+ plaintext: { inline: "Blocked by IP" },
66
+ default: "plaintext"
67
+ }
68
+ get("/z") { |r| r.ok "never" }
69
+ end
70
+ ) do
71
+ res = get_resp("/z")
72
+ assert_equal "403", res.code
73
+ assert_equal "Blocked by IP", res.body
74
+ end
75
+ end
76
+
77
+ # 4. Trusted proxies: extract client IP from header if proxy is trusted
78
+ def test_trusted_proxy_denies_based_on_header
79
+ server(
80
+ itsi_rb: lambda do
81
+ deny_list \
82
+ denied_patterns: ["^198\\.51\\.100\\.9$"],
83
+ trusted_proxies: {
84
+ "127.0.0.1" => { header: { name: "X-Forwarded-For" } }
85
+ }
86
+ get("/deny") { |r| r.ok "shouldn't see" }
87
+ end
88
+ ) do
89
+ # Header says user is 198.51.100.9, which is denied
90
+ res = get_resp("/deny", { "X-Forwarded-For" => "198.51.100.9" })
91
+ assert_equal "403", res.code
92
+ end
93
+ end
94
+
95
+ def test_trusted_proxy_allows_if_forwarded_ip_not_denied
96
+ server(
97
+ itsi_rb: lambda do
98
+ deny_list \
99
+ denied_patterns: ["^198\\.51\\.100\\.9$"], # deny this IP
100
+ trusted_proxies: {
101
+ "127.0.0.1" => { header: { name: "X-Forwarded-For" } }
102
+ }
103
+ get("/deny") { |r| r.ok "welcome" }
104
+ end
105
+ ) do
106
+ # Header says user is 198.51.100.10, which is not denied
107
+ res = get_resp("/deny", { "X-Forwarded-For" => "198.51.100.10" })
108
+ assert_equal "200", res.code
109
+ assert_equal "welcome", res.body
110
+ end
111
+ end
112
+
113
+ def test_untrusted_proxy_ignores_forwarded_ip
114
+ server(
115
+ itsi_rb: lambda do
116
+ deny_list \
117
+ denied_patterns: ["^198\\.51\\.100\\.9$"],
118
+ trusted_proxies: {
119
+ "10.0.0.1" => { header: { name: "X-Forwarded-For" } } # current sender not trusted
120
+ }
121
+ get("/deny") { |r| r.ok "should be denied by socket IP" }
122
+ end
123
+ ) do
124
+ # Because 127.0.0.1 is not trusted, header is ignored and socket IP (which matches ^127) is not denied
125
+ # But 127.0.0.1 isn't in the denied_patterns, so it's allowed
126
+ res = get_resp("/deny", { "X-Forwarded-For" => "198.51.100.9" })
127
+ assert_equal "200", res.code
128
+ assert_equal "should be denied by socket IP", res.body
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,300 @@
1
+ require_relative "../helpers/test_helper"
2
+ require "json"
3
+
4
+ MySchema = { _required: %i[name], name: String }
5
+ PingSchema = { _required: %i[status], status: String }
6
+
7
+
8
+ class TestEndpoint < Minitest::Test
9
+ # 1. Basic inline GET endpoint
10
+ def test_basic_get
11
+ server(itsi_rb: lambda do
12
+ endpoint "/hello" do |req|
13
+ req.ok "world"
14
+ end
15
+ end) do
16
+ res = get_resp("/hello")
17
+ assert_equal "200", res.code
18
+ assert_equal "world", res.body
19
+ end
20
+ end
21
+
22
+ # 2. GET-only wrapper rejects POST
23
+ def test_get_wrapper_rejects_post
24
+ server(itsi_rb: lambda do
25
+ get "/onlyget" do |req|
26
+ req.ok "got"
27
+ end
28
+ end) do
29
+ assert_equal "got", get("/onlyget")
30
+ post_res = post("/onlyget", "")
31
+ refute_equal post_res.body, "got"
32
+ end
33
+ end
34
+
35
+ # 3. Trailing slash handling: both /path and /path/ map
36
+ def test_trailing_slash_equivalence
37
+ server(itsi_rb: lambda do
38
+ endpoint "/users" do |req|
39
+ req.ok req.path
40
+ end
41
+ end) do
42
+ res1 = get_resp("/users")
43
+ assert_equal "200", res1.code
44
+ assert_equal "/users", res1.body
45
+
46
+ res2 = get_resp("/users/")
47
+ assert_equal "200", res2.code
48
+ assert_equal "/users/", res2.body
49
+ end
50
+ end
51
+
52
+ # 4. Empty‑path endpoint: wildcard match all paths
53
+ def test_empty_path_wildcard
54
+ server(itsi_rb: lambda do
55
+ endpoint do |req|
56
+ req.ok req.path
57
+ end
58
+ end) do
59
+ %w[/ /foo /foo/bar].each do |path|
60
+ res = get_resp(path)
61
+ assert_equal "200", res.code
62
+ assert_equal path, res.body
63
+ end
64
+ end
65
+ end
66
+
67
+ # 5. JSON body parsing into params
68
+ def test_json_body_parsing
69
+ server(itsi_rb: lambda do
70
+ endpoint "/echo_json" do |req, params|
71
+ req.ok JSON.generate(params)
72
+ end
73
+ end) do
74
+ payload = { "a" => 1, "b" => 2 }
75
+ res = post(
76
+ "/echo_json",
77
+ JSON.generate(payload),
78
+ "Content-Type" => "application/json"
79
+ )
80
+ assert_equal "200", res.code
81
+ assert_equal payload, JSON.parse(res.body)
82
+ end
83
+ end
84
+
85
+ # 6. Form‑encoded body parsing
86
+ def test_form_urlencoded_parsing
87
+ server(itsi_rb: lambda do
88
+ endpoint "/echo_form" do |req, params|
89
+ req.ok "#{params['x']}-#{params['y']}"
90
+ end
91
+ end) do
92
+ res = post(
93
+ "/echo_form",
94
+ "x=foo&y=bar",
95
+ "Content-Type" => "application/x-www-form-urlencoded"
96
+ )
97
+ assert_equal "200", res.code
98
+ assert_equal "foo-bar", res.body
99
+ end
100
+ end
101
+
102
+ # 7. Streaming via low‑level response object
103
+ def test_streaming_response
104
+ server(itsi_rb: lambda do
105
+ endpoint "/stream" do |req|
106
+ stream = req.response
107
+ stream << "A"
108
+ stream << "B"
109
+ stream << "C"
110
+ stream.close
111
+ end
112
+ end) do
113
+ res = get_resp("/stream")
114
+ assert_equal "200", res.code
115
+ assert_equal "ABC", res.body
116
+ end
117
+ end
118
+
119
+ # 8. Exceptions inside handler produce 500
120
+ def test_internal_error_returns_500
121
+ server(itsi_rb: lambda do
122
+ endpoint "/boom" do |_req|
123
+ raise "test crash"
124
+ end
125
+ end) do
126
+ res = get_resp("/boom")
127
+ assert_equal "500", res.code
128
+ end
129
+ end
130
+
131
+ # 9. Controller method dispatch via symbol
132
+ def test_controller_symbol_dispatch
133
+ server(itsi_rb: lambda do
134
+ # define handler method in top‑level scope
135
+ def foo_handler(req)
136
+ req.ok "from_symbol"
137
+ end
138
+ endpoint "/sym", :foo_handler
139
+ end) do
140
+ res = get_resp("/sym")
141
+ assert_equal "200", res.code
142
+ assert_equal "from_symbol", res.body
143
+ end
144
+ end
145
+
146
+ # 10. Schema enforcement rejects bad JSON
147
+ def test_schema_validation_failure
148
+ server(itsi_rb: lambda do
149
+ # declare a schema and use typed params
150
+ endpoint "/typed" do |req, params: MySchema|
151
+ req.ok params["name"]
152
+ end
153
+ end) do
154
+ # missing "name"
155
+ res = post(
156
+ "/typed",
157
+ "{}",
158
+ "Content-Type" => "application/json"
159
+ )
160
+ assert_equal "400", res.code
161
+ assert_match /Validation failed/, res.body
162
+ end
163
+ end
164
+
165
+ # 11. Response‑format schema enforcement
166
+ def test_response_schema_enforcement
167
+ server(itsi_rb: lambda do
168
+ endpoint "/ping" do |req, _params, response_format: PingSchema|
169
+ req.ok json: { "wrong" => 123 }, as: response_format
170
+ end
171
+ end) do
172
+ res = get_resp("/ping")
173
+ assert_equal "400", res.code
174
+ assert_match /Validation failed/, res.body
175
+ end
176
+ end
177
+
178
+ # 12. Unrestricted HTTP method on endpoint (no wrapper)
179
+ def test_unrestricted_endpoint_all_methods
180
+ server(itsi_rb: lambda do
181
+ endpoint "/all" do |req|
182
+ req.ok req.request_method
183
+ end
184
+ end) do
185
+ %w[POST PUT PATCH DELETE].each do |m|
186
+ resp = send(m.downcase, "/all")
187
+ assert_equal "200", resp.code
188
+ assert_equal m, resp.body
189
+ end
190
+ resp = get_resp("/all")
191
+ assert_equal "200", resp.code
192
+ assert_equal "GET", resp.body
193
+ end
194
+ end
195
+
196
+ def test_head_on_get_wrapper
197
+ server(itsi_rb: lambda do
198
+ get "/resource" do |req|
199
+ req.respond body: "payload", status: 200, headers: { "X-Foo" => "bar" }
200
+ end
201
+ end) do
202
+ res = head("/resource")
203
+ assert_equal "200", res.code
204
+ assert_equal "bar", res["X-Foo"]
205
+ assert_empty res.body.to_s
206
+ end
207
+ end
208
+
209
+ # 14. POST wrapper accepts POST only
210
+ def test_post_wrapper
211
+ server(itsi_rb: lambda do
212
+ post "/submit" do |req|
213
+ req.ok "posted"
214
+ end
215
+ endpoint do |req|
216
+ req.not_found ""
217
+ end
218
+ end) do
219
+ assert_equal "posted", post("/submit", "").body
220
+ assert_equal "404", get_resp("/submit").code
221
+ assert_equal "404", head("/submit").code
222
+ assert_equal "404", options("/submit").code
223
+ end
224
+ end
225
+
226
+ # 15. PUT, PATCH, DELETE wrappers
227
+ %w[put patch delete].each do |verb|
228
+ define_method("test_#{verb}_wrapper") do
229
+ server(itsi_rb: lambda do
230
+ send(verb, "/thing") do |req|
231
+ req.ok req.request_method
232
+ end
233
+ endpoint do |req|
234
+ req.not_found ""
235
+ end
236
+ end) do
237
+ # correct method
238
+ send("#{verb}", "/thing")
239
+ # mismatched methods
240
+ %w[POST OPTIONS HEAD].each do |bad|
241
+ res = send("#{bad.downcase}", "/thing")
242
+ assert_equal "404", res.code, "#{verb.upcase} wrapper should 404 on #{bad}"
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ # 16. OPTIONS on unrestricted endpoint yields 200
249
+ def test_options_on_unrestricted
250
+ server(itsi_rb: lambda do
251
+ endpoint "/open" do |req|
252
+ req.ok "ok"
253
+ end
254
+ end) do
255
+ res = options("/open")
256
+ assert_equal "200", res.code
257
+ assert_equal "ok", res.body
258
+ end
259
+ end
260
+
261
+ # 17. HEAD on streaming endpoint
262
+ def test_head_on_streaming_endpoint
263
+ server(itsi_rb: lambda do
264
+ endpoint "/stream" do |req|
265
+ s = req.response
266
+ s << "XYZ"
267
+ s.close
268
+ end
269
+ end) do
270
+ res = head("/stream")
271
+ assert_equal "200", res.code
272
+ assert_empty res.body.to_s
273
+ end
274
+ end
275
+
276
+ # 18. Wildcard segment matching
277
+ def test_wildcard_endpoint
278
+ server(itsi_rb: lambda do
279
+ endpoint "/api/*" do |req|
280
+ req.ok req.path
281
+ end
282
+ end) do
283
+ ["/api/foo", "/api/foo/bar", "/api/"].each do |path|
284
+ res = get_resp(path)
285
+ assert_equal path, res.body
286
+ end
287
+ end
288
+ end
289
+
290
+ # 19. Multiple endpoints same path, different verbs
291
+ def test_same_path_different_verb
292
+ server(itsi_rb: lambda do
293
+ get "/duo" do |req| req.ok "g" end
294
+ post "/duo" do |req| req.ok "p" end
295
+ end) do
296
+ assert_equal "g", get("/duo")
297
+ assert_equal "p", post("/duo", "").body
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,75 @@
1
+ require_relative "../helpers/test_helper"
2
+
3
+ class TestETag < Minitest::Test
4
+ def test_strong_etag_added
5
+ server(
6
+ itsi_rb: lambda do
7
+ etag type: "strong", algorithm: "sha256", min_body_size: 0, handle_if_none_match: false
8
+ get("/foo") { |r| r.ok "etag-body" }
9
+ end
10
+ ) do
11
+ res = get_resp("/foo")
12
+ assert res["ETag"].start_with?("\"")
13
+ refute res["ETag"].start_with?("W/")
14
+ end
15
+ end
16
+
17
+ def test_weak_etag_added_with_md5
18
+ server(
19
+ itsi_rb: lambda do
20
+ etag type: "weak", algorithm: "md5", min_body_size: 0, handle_if_none_match: false
21
+ get("/foo") { |r| r.ok "etag-weak-md5" }
22
+ end
23
+ ) do
24
+ res = get_resp("/foo")
25
+ assert res["ETag"].start_with?("W/\"")
26
+ end
27
+ end
28
+
29
+ def test_etag_not_added_below_min_body_size
30
+ server(
31
+ itsi_rb: lambda do
32
+ etag min_body_size: 50
33
+ get("/foo") { |r| r.ok "short" }
34
+ end
35
+ ) do
36
+ res = get_resp("/foo")
37
+ refute res.key?("ETag")
38
+ end
39
+ end
40
+
41
+ def test_etag_responds_with_304_if_none_match
42
+ body = "etag-content"
43
+ server(
44
+ itsi_rb: lambda do
45
+ etag type: "strong", min_body_size: 0, handle_if_none_match: true
46
+ get("/foo") { |r| r.ok body }
47
+ end
48
+ ) do
49
+ first = get_resp("/foo")
50
+ etag = first["ETag"]
51
+ assert etag
52
+
53
+ second = get_resp("/foo", { "If-None-Match" => etag })
54
+ assert_equal "304", second.code
55
+ refute second.body && second.body.size > 0
56
+ end
57
+ end
58
+
59
+ def test_etag_ignored_for_streaming_body
60
+ server(
61
+ itsi_rb: lambda do
62
+ etag min_body_size: 0
63
+ get("/stream") do |req|
64
+ r = req.response
65
+ r << "streaming-1"
66
+ r << "streaming-2"
67
+ r.close
68
+ end
69
+ end
70
+ ) do
71
+ res = get_resp("/stream")
72
+ refute res.key?("ETag")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,158 @@
1
+ require_relative "../../helpers/test_helper"
2
+ require_relative "test_service_impl"
3
+
4
+ class TestGrpc < Minitest::Test
5
+ Stub = Test::TestService::Stub
6
+
7
+ def new_stub(uri)
8
+ address = uri.to_s[/\/\/(.*)/,1] # e.g. "127.0.0.1:12345"
9
+ channel = GRPC::Core::Channel.new(address,
10
+ {}, # channel_args hash
11
+ :this_channel_is_insecure,
12
+ )
13
+ stub = Stub.new(nil,
14
+ nil,
15
+ channel_override: channel)
16
+ end
17
+
18
+ def test_unary_echo
19
+ server(itsi_rb: lambda do
20
+ grpc TestServiceImpl.new
21
+ end) do
22
+ stub = new_stub(@uri)
23
+ req = Test::EchoRequest.new(message: "hello")
24
+ res = stub.unary_echo(req)
25
+ assert_equal "hello", res.message
26
+ end
27
+ end
28
+
29
+ def test_client_stream
30
+ server(itsi_rb: lambda do
31
+ grpc TestServiceImpl.new
32
+ end) do
33
+ stub = new_stub(@uri)
34
+ # send a few messages in one stream
35
+ requests = %w[a b c].map { |m| Test::StreamRequest.new(message: m) }
36
+ res = stub.client_stream(requests.each)
37
+ assert_equal %w[a b c], res.messages
38
+ end
39
+ end
40
+
41
+ def test_server_stream
42
+ server(itsi_rb: lambda do
43
+ grpc TestServiceImpl.new
44
+ end) do
45
+ stub = new_stub(@uri)
46
+ req = Test::EchoRequest.new(message: "xy")
47
+ # expect two StreamResponse frames, one per character
48
+ chars = stub.server_stream(req).map(&:messages).flatten
49
+ assert_equal ["x", "y"], chars
50
+ end
51
+ end
52
+
53
+ def test_bidi_stream
54
+ server(itsi_rb: lambda do
55
+ grpc TestServiceImpl.new
56
+ end) do
57
+ stub = new_stub(@uri)
58
+ inputs = %w[test1 test2].map { |m| Test::EchoRequest.new(message: m) }
59
+ # each response uppercases its incoming message
60
+ results = stub.bidi_stream(inputs.each).map(&:message)
61
+ assert_equal ["TEST1", "TEST2"], results
62
+ end
63
+ end
64
+
65
+ def test_empty_streams
66
+ server(itsi_rb: lambda do
67
+ grpc TestServiceImpl.new
68
+ end) do
69
+ stub = new_stub(@uri)
70
+
71
+ # empty client‐streaming
72
+ res = stub.client_stream([].each)
73
+ assert_equal [], res.messages
74
+
75
+ # empty server‐streaming
76
+ empty_server = stub.server_stream(Test::EchoRequest.new(message: "")).to_a
77
+ assert empty_server.empty?
78
+
79
+ # empty bidi‐streaming
80
+ empty_bidi = stub.bidi_stream([].each).to_a
81
+ assert empty_bidi.empty?
82
+ end
83
+ end
84
+
85
+
86
+ def test_unary_echo_json
87
+ server(itsi_rb: lambda do
88
+ grpc TestServiceImpl.new
89
+ end) do
90
+ payload = { "message" => "hello" }.to_json
91
+ res = post("test.TestService/UnaryEcho", payload, { "Content-Type" => "application/json" })
92
+ json = JSON.parse(res.body)
93
+ assert_equal 200, res.code.to_i
94
+ assert_equal "hello", json["message"]
95
+ end
96
+ end
97
+
98
+ def test_client_stream_json
99
+ server(itsi_rb: lambda do
100
+ grpc TestServiceImpl.new
101
+ end) do
102
+ input = [{"message" => "a"}, {"message" => "b"}, {"message" => "c"}]
103
+ res = post("test.TestService/ClientStream", input.to_json, { "Content-Type" => "application/json" })
104
+ json = JSON.parse(res.body)
105
+ assert_equal 200, res.code.to_i
106
+ assert_equal %w[a b c], json["messages"]
107
+ end
108
+ end
109
+
110
+ def test_server_stream_json
111
+ server(itsi_rb: lambda do
112
+ grpc TestServiceImpl.new
113
+ end) do
114
+ payload = { "message" => "xy" }.to_json
115
+ res = post("test.TestService/ServerStream", payload, { "Content-Type" => "application/json" })
116
+ arr = JSON.parse(res.body)
117
+ # service streams back one JSON object per character
118
+ messages = arr.flat_map { |frame| frame["messages"] }
119
+ assert_equal 200, res.code.to_i
120
+ assert_equal ["x","y"], messages
121
+ end
122
+ end
123
+
124
+ def test_bidi_stream_json
125
+ server(itsi_rb: lambda do
126
+ grpc TestServiceImpl.new
127
+ end) do
128
+ inputs = [{"message":"foo"}, {"message":"Bar"}]
129
+ res = post("test.TestService/BidiStream", inputs.to_json, { "Content-Type" => "application/json" })
130
+ arr = JSON.parse(res.body)
131
+ # bidi uppercases each incoming message
132
+ results = arr.map { |frame| frame["message"] }
133
+ assert_equal 200, res.code.to_i
134
+ assert_equal ["FOO","BAR"], results
135
+ end
136
+ end
137
+
138
+ def test_empty_streams_json
139
+ server(itsi_rb: lambda do
140
+ grpc TestServiceImpl.new
141
+ end) do
142
+ # empty client-stream → still JSON object with empty messages
143
+ res1 = post("test.TestService/ClientStream", "[]", { "Content-Type" => "application/json" })
144
+ json1 = JSON.parse(res1.body)
145
+ assert_nil json1["messages"]
146
+
147
+ # empty server-stream
148
+ res2 = post("test.TestService/ServerStream", { "message" => "" }.to_json, { "Content-Type" => "application/json" })
149
+ arr2 = JSON.parse(res2.body)
150
+ assert arr2.empty?
151
+
152
+ # empty bidi-stream
153
+ res3 = post("test.TestService/BidiStream", "[]", { "Content-Type" => "application/json" })
154
+ arr3 = JSON.parse(res3.body)
155
+ assert arr3.empty?
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,32 @@
1
+ syntax = "proto3";
2
+
3
+ package test;
4
+
5
+ // A service with one of each RPC type [oai_citation_attribution:0‡Википедия — свободная энциклопедия](https://ru.wikipedia.org/wiki/GRPC?utm_source=chatgpt.com)
6
+ service TestService {
7
+ // Unary RPC
8
+ rpc UnaryEcho (EchoRequest) returns (EchoResponse) {}
9
+
10
+ // Client‑streaming RPC
11
+ rpc ClientStream (stream StreamRequest) returns (StreamResponse) {}
12
+
13
+ // Server‑streaming RPC
14
+ rpc ServerStream (EchoRequest) returns (stream StreamResponse) {}
15
+
16
+ // Bidirectional streaming RPC
17
+ rpc BidiStream (stream EchoRequest) returns (stream EchoResponse) {}
18
+ }
19
+
20
+ message EchoRequest {
21
+ string message = 1;
22
+ }
23
+ message EchoResponse {
24
+ string message = 1;
25
+ }
26
+
27
+ message StreamRequest {
28
+ string message = 1;
29
+ }
30
+ message StreamResponse {
31
+ repeated string messages = 1;
32
+ }