reqcord 0.1.0 → 0.1.1

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/CHANGELOG.md +77 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +171 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +239 -95
  8. data/Rakefile +13 -0
  9. data/docs/configuration.md +319 -0
  10. data/examples/complex-spec-app/.rspec +1 -0
  11. data/examples/complex-spec-app/README.md +21 -0
  12. data/examples/complex-spec-app/app.rb +363 -0
  13. data/examples/complex-spec-app/docs/api/README.md +104 -0
  14. data/examples/complex-spec-app/docs/api/api/v1/admin/products/create.md +117 -0
  15. data/examples/complex-spec-app/docs/api/api/v1/admin/products/destroy.md +51 -0
  16. data/examples/complex-spec-app/docs/api/api/v1/admin/products/index.md +11 -0
  17. data/examples/complex-spec-app/docs/api/api/v1/admin/products/list.md +106 -0
  18. data/examples/complex-spec-app/docs/api/api/v1/auth/index.md +9 -0
  19. data/examples/complex-spec-app/docs/api/api/v1/auth/login.md +76 -0
  20. data/examples/complex-spec-app/docs/api/api/v1/cart-items/create.md +91 -0
  21. data/examples/complex-spec-app/docs/api/api/v1/cart-items/destroy.md +51 -0
  22. data/examples/complex-spec-app/docs/api/api/v1/cart-items/index.md +10 -0
  23. data/examples/complex-spec-app/docs/api/api/v1/carts/checkout.md +102 -0
  24. data/examples/complex-spec-app/docs/api/api/v1/carts/index.md +10 -0
  25. data/examples/complex-spec-app/docs/api/api/v1/carts/show.md +73 -0
  26. data/examples/complex-spec-app/docs/api/api/v1/home/index.md +9 -0
  27. data/examples/complex-spec-app/docs/api/api/v1/home/list.md +47 -0
  28. data/examples/complex-spec-app/docs/api/api/v1/notes/create.md +94 -0
  29. data/examples/complex-spec-app/docs/api/api/v1/notes/index.md +10 -0
  30. data/examples/complex-spec-app/docs/api/api/v1/notes/list.md +61 -0
  31. data/examples/complex-spec-app/docs/api/api/v1/orders/cancel.md +69 -0
  32. data/examples/complex-spec-app/docs/api/api/v1/orders/create.md +150 -0
  33. data/examples/complex-spec-app/docs/api/api/v1/orders/destroy.md +37 -0
  34. data/examples/complex-spec-app/docs/api/api/v1/orders/index.md +13 -0
  35. data/examples/complex-spec-app/docs/api/api/v1/orders/list.md +78 -0
  36. data/examples/complex-spec-app/docs/api/api/v1/orders/show.md +83 -0
  37. data/examples/complex-spec-app/docs/api/api/v1/products/index.md +11 -0
  38. data/examples/complex-spec-app/docs/api/api/v1/products/list.md +83 -0
  39. data/examples/complex-spec-app/docs/api/api/v1/products/search.md +88 -0
  40. data/examples/complex-spec-app/docs/api/api/v1/products/show.md +71 -0
  41. data/examples/complex-spec-app/docs/api/api/v1/profiles/index.md +10 -0
  42. data/examples/complex-spec-app/docs/api/api/v1/profiles/show.md +61 -0
  43. data/examples/complex-spec-app/docs/api/api/v1/profiles/update.md +91 -0
  44. data/examples/complex-spec-app/docs/api/api/v2/products/index.md +9 -0
  45. data/examples/complex-spec-app/docs/api/api/v2/products/list.md +77 -0
  46. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
  47. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
  48. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
  49. data/examples/complex-spec-app/docs/api/curl/api/v1/auth/login.sh +9 -0
  50. data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
  51. data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
  52. data/examples/complex-spec-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
  53. data/examples/complex-spec-app/docs/api/curl/api/v1/carts/show.sh +9 -0
  54. data/examples/complex-spec-app/docs/api/curl/api/v1/home/list.sh +8 -0
  55. data/examples/complex-spec-app/docs/api/curl/api/v1/notes/create.sh +15 -0
  56. data/examples/complex-spec-app/docs/api/curl/api/v1/notes/list.sh +9 -0
  57. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
  58. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/create.sh +29 -0
  59. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
  60. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/list.sh +9 -0
  61. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/show.sh +9 -0
  62. data/examples/complex-spec-app/docs/api/curl/api/v1/products/list.sh +8 -0
  63. data/examples/complex-spec-app/docs/api/curl/api/v1/products/search.sh +8 -0
  64. data/examples/complex-spec-app/docs/api/curl/api/v1/products/show.sh +8 -0
  65. data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
  66. data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
  67. data/examples/complex-spec-app/docs/api/curl/api/v2/products/list.sh +8 -0
  68. data/examples/complex-spec-app/docs/api/dataset.json +5116 -0
  69. data/examples/complex-spec-app/docs/api/postman/collection.json +2519 -0
  70. data/examples/complex-spec-app/generate.rb +14 -0
  71. data/examples/complex-spec-app/reqcord.yml +28 -0
  72. data/examples/complex-spec-app/spec/requests/admin_spec.rb +67 -0
  73. data/examples/complex-spec-app/spec/requests/cart_spec.rb +61 -0
  74. data/examples/complex-spec-app/spec/requests/home_and_auth_spec.rb +49 -0
  75. data/examples/complex-spec-app/spec/requests/orders_spec.rb +124 -0
  76. data/examples/complex-spec-app/spec/requests/products_spec.rb +65 -0
  77. data/examples/complex-spec-app/spec/spec_helper.rb +17 -0
  78. data/examples/complex-test-app/README.md +40 -0
  79. data/examples/complex-test-app/app.rb +384 -0
  80. data/examples/complex-test-app/docs/api/README.md +104 -0
  81. data/examples/complex-test-app/docs/api/api/v1/admin/products/create.md +117 -0
  82. data/examples/complex-test-app/docs/api/api/v1/admin/products/destroy.md +51 -0
  83. data/examples/complex-test-app/docs/api/api/v1/admin/products/index.md +11 -0
  84. data/examples/complex-test-app/docs/api/api/v1/admin/products/list.md +106 -0
  85. data/examples/complex-test-app/docs/api/api/v1/auth/index.md +9 -0
  86. data/examples/complex-test-app/docs/api/api/v1/auth/login.md +76 -0
  87. data/examples/complex-test-app/docs/api/api/v1/cart-items/create.md +91 -0
  88. data/examples/complex-test-app/docs/api/api/v1/cart-items/destroy.md +51 -0
  89. data/examples/complex-test-app/docs/api/api/v1/cart-items/index.md +10 -0
  90. data/examples/complex-test-app/docs/api/api/v1/carts/checkout.md +102 -0
  91. data/examples/complex-test-app/docs/api/api/v1/carts/index.md +10 -0
  92. data/examples/complex-test-app/docs/api/api/v1/carts/show.md +73 -0
  93. data/examples/complex-test-app/docs/api/api/v1/home/index.md +9 -0
  94. data/examples/complex-test-app/docs/api/api/v1/home/list.md +47 -0
  95. data/examples/complex-test-app/docs/api/api/v1/notes/create.md +94 -0
  96. data/examples/complex-test-app/docs/api/api/v1/notes/index.md +10 -0
  97. data/examples/complex-test-app/docs/api/api/v1/notes/list.md +61 -0
  98. data/examples/complex-test-app/docs/api/api/v1/orders/cancel.md +69 -0
  99. data/examples/complex-test-app/docs/api/api/v1/orders/create.md +150 -0
  100. data/examples/complex-test-app/docs/api/api/v1/orders/destroy.md +37 -0
  101. data/examples/complex-test-app/docs/api/api/v1/orders/index.md +13 -0
  102. data/examples/complex-test-app/docs/api/api/v1/orders/list.md +71 -0
  103. data/examples/complex-test-app/docs/api/api/v1/orders/show.md +83 -0
  104. data/examples/complex-test-app/docs/api/api/v1/products/index.md +11 -0
  105. data/examples/complex-test-app/docs/api/api/v1/products/list.md +83 -0
  106. data/examples/complex-test-app/docs/api/api/v1/products/search.md +88 -0
  107. data/examples/complex-test-app/docs/api/api/v1/products/show.md +71 -0
  108. data/examples/complex-test-app/docs/api/api/v1/profiles/index.md +10 -0
  109. data/examples/complex-test-app/docs/api/api/v1/profiles/show.md +61 -0
  110. data/examples/complex-test-app/docs/api/api/v1/profiles/update.md +91 -0
  111. data/examples/complex-test-app/docs/api/api/v2/products/index.md +9 -0
  112. data/examples/complex-test-app/docs/api/api/v2/products/list.md +77 -0
  113. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
  114. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
  115. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
  116. data/examples/complex-test-app/docs/api/curl/api/v1/auth/login.sh +9 -0
  117. data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
  118. data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
  119. data/examples/complex-test-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
  120. data/examples/complex-test-app/docs/api/curl/api/v1/carts/show.sh +9 -0
  121. data/examples/complex-test-app/docs/api/curl/api/v1/home/list.sh +8 -0
  122. data/examples/complex-test-app/docs/api/curl/api/v1/notes/create.sh +15 -0
  123. data/examples/complex-test-app/docs/api/curl/api/v1/notes/list.sh +9 -0
  124. data/examples/complex-test-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
  125. data/examples/complex-test-app/docs/api/curl/api/v1/orders/create.sh +29 -0
  126. data/examples/complex-test-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
  127. data/examples/complex-test-app/docs/api/curl/api/v1/orders/list.sh +9 -0
  128. data/examples/complex-test-app/docs/api/curl/api/v1/orders/show.sh +9 -0
  129. data/examples/complex-test-app/docs/api/curl/api/v1/products/list.sh +8 -0
  130. data/examples/complex-test-app/docs/api/curl/api/v1/products/search.sh +8 -0
  131. data/examples/complex-test-app/docs/api/curl/api/v1/products/show.sh +8 -0
  132. data/examples/complex-test-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
  133. data/examples/complex-test-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
  134. data/examples/complex-test-app/docs/api/curl/api/v2/products/list.sh +8 -0
  135. data/examples/complex-test-app/docs/api/dataset.json +5109 -0
  136. data/examples/complex-test-app/docs/api/postman/collection.json +2568 -0
  137. data/examples/complex-test-app/generate.rb +14 -0
  138. data/examples/complex-test-app/reqcord.yml +28 -0
  139. data/examples/complex-test-app/test/integration/admin_test.rb +70 -0
  140. data/examples/complex-test-app/test/integration/cart_test.rb +64 -0
  141. data/examples/complex-test-app/test/integration/home_and_auth_test.rb +53 -0
  142. data/examples/complex-test-app/test/integration/orders_test.rb +127 -0
  143. data/examples/complex-test-app/test/integration/products_test.rb +66 -0
  144. data/examples/reqcord.yml +58 -0
  145. data/examples/spec-app/.rspec +1 -0
  146. data/examples/spec-app/README.md +36 -0
  147. data/examples/spec-app/app.rb +209 -0
  148. data/examples/spec-app/docs/api/README.md +45 -0
  149. data/examples/spec-app/docs/api/api/v1/customers/create.md +95 -0
  150. data/examples/spec-app/docs/api/api/v1/customers/index.md +11 -0
  151. data/examples/spec-app/docs/api/api/v1/customers/list.md +67 -0
  152. data/examples/spec-app/docs/api/api/v1/customers/show.md +65 -0
  153. data/examples/spec-app/docs/api/api/v1/tasks/complete.md +51 -0
  154. data/examples/spec-app/docs/api/api/v1/tasks/create.md +90 -0
  155. data/examples/spec-app/docs/api/api/v1/tasks/destroy.md +35 -0
  156. data/examples/spec-app/docs/api/api/v1/tasks/index.md +14 -0
  157. data/examples/spec-app/docs/api/api/v1/tasks/list.md +65 -0
  158. data/examples/spec-app/docs/api/api/v1/tasks/show.md +65 -0
  159. data/examples/spec-app/docs/api/api/v1/tasks/update.md +74 -0
  160. data/examples/spec-app/docs/api/api/v1/users/create.md +95 -0
  161. data/examples/spec-app/docs/api/api/v1/users/index.md +11 -0
  162. data/examples/spec-app/docs/api/api/v1/users/list.md +67 -0
  163. data/examples/spec-app/docs/api/api/v1/users/show.md +65 -0
  164. data/examples/spec-app/docs/api/curl/api/v1/customers/create.sh +17 -0
  165. data/examples/spec-app/docs/api/curl/api/v1/customers/list.sh +9 -0
  166. data/examples/spec-app/docs/api/curl/api/v1/customers/show.sh +9 -0
  167. data/examples/spec-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
  168. data/examples/spec-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
  169. data/examples/spec-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
  170. data/examples/spec-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
  171. data/examples/spec-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
  172. data/examples/spec-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
  173. data/examples/spec-app/docs/api/curl/api/v1/users/create.sh +17 -0
  174. data/examples/spec-app/docs/api/curl/api/v1/users/list.sh +9 -0
  175. data/examples/spec-app/docs/api/curl/api/v1/users/show.sh +9 -0
  176. data/examples/spec-app/docs/api/dataset.json +2402 -0
  177. data/examples/spec-app/docs/api/postman/collection.json +1290 -0
  178. data/examples/spec-app/generate.rb +14 -0
  179. data/examples/spec-app/reqcord.yml +27 -0
  180. data/examples/spec-app/spec/requests/customers_spec.rb +71 -0
  181. data/examples/spec-app/spec/requests/tasks_spec.rb +78 -0
  182. data/examples/spec-app/spec/requests/users_spec.rb +68 -0
  183. data/examples/spec-app/spec/spec_helper.rb +17 -0
  184. data/examples/test-app/README.md +57 -0
  185. data/examples/test-app/app.rb +212 -0
  186. data/examples/test-app/docs/api/README.md +45 -0
  187. data/examples/test-app/docs/api/api/v1/customers/create.md +95 -0
  188. data/examples/test-app/docs/api/api/v1/customers/index.md +11 -0
  189. data/examples/test-app/docs/api/api/v1/customers/list.md +67 -0
  190. data/examples/test-app/docs/api/api/v1/customers/show.md +65 -0
  191. data/examples/test-app/docs/api/api/v1/tasks/complete.md +51 -0
  192. data/examples/test-app/docs/api/api/v1/tasks/create.md +90 -0
  193. data/examples/test-app/docs/api/api/v1/tasks/destroy.md +35 -0
  194. data/examples/test-app/docs/api/api/v1/tasks/index.md +14 -0
  195. data/examples/test-app/docs/api/api/v1/tasks/list.md +65 -0
  196. data/examples/test-app/docs/api/api/v1/tasks/show.md +65 -0
  197. data/examples/test-app/docs/api/api/v1/tasks/update.md +74 -0
  198. data/examples/test-app/docs/api/api/v1/users/create.md +95 -0
  199. data/examples/test-app/docs/api/api/v1/users/index.md +11 -0
  200. data/examples/test-app/docs/api/api/v1/users/list.md +67 -0
  201. data/examples/test-app/docs/api/api/v1/users/show.md +65 -0
  202. data/examples/test-app/docs/api/curl/api/v1/customers/create.sh +17 -0
  203. data/examples/test-app/docs/api/curl/api/v1/customers/list.sh +9 -0
  204. data/examples/test-app/docs/api/curl/api/v1/customers/show.sh +9 -0
  205. data/examples/test-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
  206. data/examples/test-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
  207. data/examples/test-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
  208. data/examples/test-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
  209. data/examples/test-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
  210. data/examples/test-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
  211. data/examples/test-app/docs/api/curl/api/v1/users/create.sh +17 -0
  212. data/examples/test-app/docs/api/curl/api/v1/users/list.sh +9 -0
  213. data/examples/test-app/docs/api/curl/api/v1/users/show.sh +9 -0
  214. data/examples/test-app/docs/api/dataset.json +2402 -0
  215. data/examples/test-app/docs/api/postman/collection.json +1290 -0
  216. data/examples/test-app/generate.rb +14 -0
  217. data/examples/test-app/reqcord.yml +28 -0
  218. data/examples/test-app/test/integration/customers_test.rb +72 -0
  219. data/examples/test-app/test/integration/tasks_test.rb +80 -0
  220. data/examples/test-app/test/integration/users_test.rb +72 -0
  221. data/lib/reqcord/capture/collector.rb +31 -0
  222. data/lib/reqcord/capture/integration_patch.rb +209 -0
  223. data/lib/reqcord/capture/minitest_context.rb +34 -0
  224. data/lib/reqcord/capture/rspec_context.rb +42 -0
  225. data/lib/reqcord/capture/test_context.rb +25 -0
  226. data/lib/reqcord/capture.rb +19 -0
  227. data/lib/reqcord/configuration.rb +198 -0
  228. data/lib/reqcord/dataset.rb +176 -0
  229. data/lib/reqcord/endpoint.rb +263 -0
  230. data/lib/reqcord/errors.rb +9 -0
  231. data/lib/reqcord/exporters/curl.rb +68 -0
  232. data/lib/reqcord/exporters/markdown.rb +295 -0
  233. data/lib/reqcord/exporters/postman.rb +206 -0
  234. data/lib/reqcord/exporters.rb +32 -0
  235. data/lib/reqcord/generator.rb +364 -0
  236. data/lib/reqcord/railtie.rb +51 -0
  237. data/lib/reqcord/renderers/curl.rb +56 -0
  238. data/lib/reqcord/renderers/payload.rb +69 -0
  239. data/lib/reqcord/request_example.rb +104 -0
  240. data/lib/reqcord/response_example.rb +72 -0
  241. data/lib/reqcord/route_collector.rb +242 -0
  242. data/lib/reqcord/sanitizers/sanitizer.rb +140 -0
  243. data/lib/reqcord/schema.rb +187 -0
  244. data/lib/reqcord/support.rb +58 -0
  245. data/lib/reqcord/version.rb +5 -0
  246. data/lib/reqcord.rb +78 -0
  247. data/lib/tasks/reqcord.rake +99 -0
  248. data/reqcord.gemspec +46 -0
  249. metadata +317 -3
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stands in for `bin/rails reqcord:generate`, which this single file example
4
+ # has no `bin/rails` to run.
5
+ require_relative "app"
6
+
7
+ dataset = Reqcord::Generator.call(
8
+ resources: ENV.fetch("RESOURCE", "").split(",").map(&:strip).reject(&:empty?),
9
+ version: ENV["VERSION"]
10
+ )
11
+
12
+ puts
13
+ puts "Endpoints: #{dataset.endpoints.size} (#{dataset.documented_endpoints.size} covered by tests)"
14
+ puts "Output: #{Reqcord.configuration.output_directory}"
@@ -0,0 +1,28 @@
1
+ version: 1
2
+
3
+ test:
4
+ framework: minitest
5
+ # A directory is enough. There is no bin/rails here, so Reqcord runs every
6
+ # *_test.rb beneath it itself; in a Rails app it runs `bin/rails test <path>`.
7
+ paths:
8
+ - test/integration
9
+
10
+ routes:
11
+ prefix: /api
12
+
13
+ output:
14
+ directory: docs/api
15
+
16
+ exporters:
17
+ - curl
18
+ - markdown
19
+ - postman
20
+
21
+ variables:
22
+ base_url: http://localhost:3000
23
+
24
+ sanitize:
25
+ headers:
26
+ Authorization: "Bearer {{token}}"
27
+ body:
28
+ password: "{{password}}"
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ordinary Rails integration tests. Nothing here mentions Reqcord: the
4
+ # documentation is generated from the requests these tests already make.
5
+ require_relative "../../app"
6
+ require "minitest/autorun"
7
+
8
+ class CustomersTest < ActionDispatch::IntegrationTest
9
+ def auth
10
+ { "Authorization" => "Bearer eyJhbGciOiJIUzI1NiJ9.super-secret" }
11
+ end
12
+
13
+ test "lists customers" do
14
+ get "/api/v1/customers", params: { per_page: 1 }, headers: auth, as: :json
15
+
16
+ assert_response :ok
17
+ end
18
+
19
+ test "shows customer" do
20
+ get "/api/v1/customers/1", headers: auth, as: :json
21
+
22
+ assert_response :ok
23
+ end
24
+
25
+ test "returns not found for an unknown customer" do
26
+ get "/api/v1/customers/999", headers: auth, as: :json
27
+
28
+ assert_response :not_found
29
+ end
30
+
31
+ test "creates an active customer" do
32
+ post "/api/v1/customers",
33
+ params: { customer: { name: "Ada Lovelace", email: "ada@example.com", status: "active" } },
34
+ headers: auth,
35
+ as: :json
36
+
37
+ assert_response :created
38
+ end
39
+
40
+ test "creates a passive customer" do
41
+ post "/api/v1/customers",
42
+ params: { customer: { name: "Grace Hopper", email: "grace@example.com", status: "passive" } },
43
+ headers: auth,
44
+ as: :json
45
+
46
+ assert_response :created
47
+ end
48
+
49
+ test "rejects an unknown status" do
50
+ post "/api/v1/customers",
51
+ params: { customer: { name: "Ada Lovelace", email: "ada@example.com", status: "inactive" } },
52
+ headers: auth,
53
+ as: :json
54
+
55
+ assert_response :unprocessable_entity
56
+ end
57
+
58
+ test "rejects a customer without an email" do
59
+ post "/api/v1/customers",
60
+ params: { customer: { name: "Ada Lovelace", email: "", status: "active" } },
61
+ headers: auth,
62
+ as: :json
63
+
64
+ assert_response :unprocessable_entity
65
+ end
66
+
67
+ test "requires authentication" do
68
+ get "/api/v1/customers", as: :json
69
+
70
+ assert_response :unauthorized
71
+ end
72
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ordinary Rails integration tests. Nothing here mentions Reqcord: the
4
+ # documentation is generated from the requests these tests already make.
5
+ require_relative "../../app"
6
+ require "minitest/autorun"
7
+
8
+ class TasksTest < ActionDispatch::IntegrationTest
9
+ test "lists tasks" do
10
+ get "/api/v1/tasks", as: :json
11
+
12
+ assert_response :ok
13
+ end
14
+
15
+ test "lists open tasks" do
16
+ get "/api/v1/tasks", params: { status: "open" }, as: :json
17
+
18
+ assert_response :ok
19
+ end
20
+
21
+ test "lists done tasks" do
22
+ get "/api/v1/tasks", params: { status: "done" }, as: :json
23
+
24
+ assert_response :ok
25
+ end
26
+
27
+ test "shows a task" do
28
+ get "/api/v1/tasks/1", as: :json
29
+
30
+ assert_response :ok
31
+ end
32
+
33
+ test "returns not found for an unknown task" do
34
+ get "/api/v1/tasks/999", as: :json
35
+
36
+ assert_response :not_found
37
+ end
38
+
39
+ test "creates a high priority task" do
40
+ post "/api/v1/tasks", params: { task: { title: "Review the release", priority: "high" } }, as: :json
41
+
42
+ assert_response :created
43
+ end
44
+
45
+ test "creates a low priority task" do
46
+ post "/api/v1/tasks", params: { task: { title: "Water the plants", priority: "low" } }, as: :json
47
+
48
+ assert_response :created
49
+ end
50
+
51
+ test "rejects a task without a title" do
52
+ post "/api/v1/tasks", params: { task: { title: "", priority: "high" } }, as: :json
53
+
54
+ assert_response :unprocessable_entity
55
+ end
56
+
57
+ test "rejects an unknown priority" do
58
+ post "/api/v1/tasks", params: { task: { title: "Anything", priority: "urgent" } }, as: :json
59
+
60
+ assert_response :unprocessable_entity
61
+ end
62
+
63
+ test "updates a task" do
64
+ patch "/api/v1/tasks/1", params: { task: { title: "Write better docs" } }, as: :json
65
+
66
+ assert_response :ok
67
+ end
68
+
69
+ test "completes a task" do
70
+ post "/api/v1/tasks/1/complete", as: :json
71
+
72
+ assert_response :ok
73
+ end
74
+
75
+ test "deletes a task" do
76
+ delete "/api/v1/tasks/3", as: :json
77
+
78
+ assert_response :no_content
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ordinary Rails integration tests. Nothing here mentions Reqcord: the
4
+ # documentation is generated from the requests these tests already make.
5
+ require_relative "../../app"
6
+ require "minitest/autorun"
7
+
8
+ class UsersTest < ActionDispatch::IntegrationTest
9
+ def auth
10
+ { "Authorization" => "Bearer eyJhbGciOiJIUzI1NiJ9.super-secret" }
11
+ end
12
+
13
+ test "lists users" do
14
+ get "/api/v1/users", params: { per_page: 1 }, headers: auth, as: :json
15
+
16
+ assert_response :ok
17
+ end
18
+
19
+ test "shows user" do
20
+ get "/api/v1/users/1", headers: auth, as: :json
21
+
22
+ assert_response :ok
23
+ end
24
+
25
+ test "returns not found for an unknown user" do
26
+ get "/api/v1/users/999", headers: auth, as: :json
27
+
28
+ assert_response :not_found
29
+ end
30
+
31
+ test "creates an active user" do
32
+ post "/api/v1/users",
33
+ params: { user: { name: "Ada Lovelace", email: "ada@example.com", status: "active" } },
34
+ headers: auth,
35
+ as: :json
36
+
37
+ assert_response :created
38
+ end
39
+
40
+ test "creates a inactive user" do
41
+ post "/api/v1/users",
42
+ params: { user: { name: "Grace Hopper", email: "grace@example.com", status: "inactive" } },
43
+ headers: auth,
44
+ as: :json
45
+
46
+ assert_response :created
47
+ end
48
+
49
+ test "rejects an unknown status" do
50
+ post "/api/v1/users",
51
+ params: { user: { name: "Ada Lovelace", email: "ada@example.com", status: "passive" } },
52
+ headers: auth,
53
+ as: :json
54
+
55
+ assert_response :unprocessable_entity
56
+ end
57
+
58
+ test "rejects a user without an email" do
59
+ post "/api/v1/users",
60
+ params: { user: { name: "Ada Lovelace", email: "", status: "active" } },
61
+ headers: auth,
62
+ as: :json
63
+
64
+ assert_response :unprocessable_entity
65
+ end
66
+
67
+ test "requires authentication" do
68
+ get "/api/v1/users", as: :json
69
+
70
+ assert_response :unauthorized
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ module Capture
5
+ # Appends one JSON line per exchange. The lock keeps parallel test workers
6
+ # from interleaving partial lines in the same file.
7
+ class Collector
8
+ class << self
9
+ def write(exchange)
10
+ return unless Capture.enabled?
11
+
12
+ path = Capture.capture_file
13
+
14
+ FileUtils.mkdir_p(File.dirname(path))
15
+
16
+ File.open(path, "a") do |file|
17
+ file.flock(File::LOCK_EX)
18
+
19
+ file.puts(
20
+ JSON.generate(exchange)
21
+ )
22
+
23
+ file.flush
24
+ ensure
25
+ file.flock(File::LOCK_UN)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ module Capture
5
+ # Captures exactly what the integration test passed to Rails. Reqcord does
6
+ # not reconstruct the payload from controller params: the test call is the
7
+ # source of truth for generated cURL examples.
8
+ module IntegrationPatch
9
+ RAILS_DEFAULT_ACCEPT =
10
+ "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
11
+
12
+ def process(method, path, **kwargs)
13
+ raw_params = kwargs[:params]
14
+ raw_headers = kwargs[:headers]
15
+ request_format = kwargs[:as]
16
+
17
+ result = super
18
+
19
+ if Reqcord::Capture.enabled?
20
+ Reqcord::Capture::Collector.write(
21
+ reqcord_exchange(
22
+ method: method,
23
+ path: path,
24
+ params: raw_params,
25
+ input_headers: raw_headers,
26
+ request_format: request_format
27
+ )
28
+ )
29
+ end
30
+
31
+ result
32
+ end
33
+
34
+ private
35
+
36
+ def reqcord_exchange(method:, path:, params:, input_headers:, request_format:)
37
+ verb = method.to_s.upcase
38
+ query_params, body = reqcord_split_params(verb, path, params)
39
+
40
+ {
41
+ request: {
42
+ method: verb,
43
+ path: reqcord_request_path(path),
44
+ path_params: reqcord_path_parameters,
45
+ query_params: query_params,
46
+ headers: reqcord_request_headers(input_headers, body, request_format),
47
+ body: body,
48
+ content_type: reqcord_content_type(body, request_format)
49
+ },
50
+ response: {
51
+ status: response&.status,
52
+ headers: reqcord_response_headers,
53
+ body: reqcord_response_body,
54
+ content_type: response&.media_type
55
+ },
56
+ source: TestContext.current
57
+ }
58
+ end
59
+
60
+ # Keep the concrete test URL (/customers/42), not the route pattern
61
+ # (/customers/:id). A generated cURL command must be runnable as-is.
62
+ def reqcord_request_path(path)
63
+ URI.parse(path.to_s).path
64
+ rescue URI::InvalidURIError
65
+ path.to_s.split("?").first
66
+ end
67
+
68
+ def reqcord_path_parameters
69
+ return {} unless request
70
+
71
+ request.path_parameters
72
+ .except(:controller, :action, :format)
73
+ .transform_keys(&:to_s)
74
+ end
75
+
76
+ # Rails integration tests use params as query parameters for GET/HEAD and
77
+ # as the request payload for mutating verbs. Read the original test input
78
+ # instead of trying to reverse-engineer it from ActionDispatch afterwards.
79
+ def reqcord_split_params(verb, path, params)
80
+ explicit_query = reqcord_query_from_path(path)
81
+ normalized = reqcord_normalize_value(params)
82
+
83
+ if %w[GET HEAD].include?(verb)
84
+ query = explicit_query
85
+ query = reqcord_deep_merge(query, normalized) if normalized.is_a?(Hash)
86
+ [query, nil]
87
+ else
88
+ [explicit_query, reqcord_meaningful?(normalized) ? normalized : nil]
89
+ end
90
+ end
91
+
92
+ def reqcord_query_from_path(path)
93
+ uri = URI.parse(path.to_s)
94
+ return {} if uri.query.nil? || uri.query.empty?
95
+
96
+ Rack::Utils.parse_nested_query(uri.query)
97
+ rescue URI::InvalidURIError
98
+ {}
99
+ end
100
+
101
+ def reqcord_request_headers(input_headers, body, request_format)
102
+ result = {}
103
+
104
+ (input_headers || {}).each do |key, value|
105
+ result[reqcord_header_name(key)] = reqcord_normalize_value(value)
106
+ end
107
+
108
+ result.delete("X-Http-Method-Override")
109
+
110
+ if body
111
+ content_type = reqcord_content_type(body, request_format)
112
+ result["Content-Type"] ||= content_type if content_type
113
+ end
114
+
115
+ if request
116
+ accept = request.headers["Accept"]
117
+ result["Accept"] ||= accept if accept.present? && accept != RAILS_DEFAULT_ACCEPT
118
+ end
119
+
120
+ result
121
+ end
122
+
123
+ def reqcord_content_type(body, request_format)
124
+ return nil unless body
125
+
126
+ return "application/json" if request_format.to_s == "json"
127
+
128
+ request&.content_type
129
+ end
130
+
131
+ def reqcord_header_name(key)
132
+ name = key.to_s
133
+
134
+ return name if name.include?("-")
135
+
136
+ name.delete_prefix("HTTP_")
137
+ .split("_")
138
+ .map { |part| part.empty? ? part : part.capitalize }
139
+ .join("-")
140
+ end
141
+
142
+ def reqcord_response_headers
143
+ return {} unless response
144
+
145
+ response.headers.to_h
146
+ end
147
+
148
+ def reqcord_response_body
149
+ return nil unless response
150
+
151
+ body = response.body
152
+ return nil if body.nil? || body.empty?
153
+
154
+ if response.media_type == "application/json"
155
+ JSON.parse(body)
156
+ else
157
+ body
158
+ end
159
+ rescue JSON::ParserError
160
+ body
161
+ end
162
+
163
+ def reqcord_normalize_value(value)
164
+ case value
165
+ when nil, true, false, Numeric, String
166
+ value
167
+ when Symbol
168
+ value.to_s
169
+ when Hash
170
+ value.each_with_object({}) do |(key, nested), result|
171
+ result[key.to_s] = reqcord_normalize_value(nested)
172
+ end
173
+ when Array
174
+ value.map { |item| reqcord_normalize_value(item) }
175
+ else
176
+ if value.respond_to?(:to_unsafe_h)
177
+ reqcord_normalize_value(value.to_unsafe_h)
178
+ elsif value.respond_to?(:to_h)
179
+ reqcord_normalize_value(value.to_h)
180
+ elsif value.respond_to?(:as_json)
181
+ reqcord_normalize_value(value.as_json)
182
+ else
183
+ value.to_s
184
+ end
185
+ end
186
+ end
187
+
188
+ def reqcord_deep_merge(left, right)
189
+ left.merge(right) do |_key, old_value, new_value|
190
+ if old_value.is_a?(Hash) && new_value.is_a?(Hash)
191
+ reqcord_deep_merge(old_value, new_value)
192
+ else
193
+ new_value
194
+ end
195
+ end
196
+ end
197
+
198
+ def reqcord_meaningful?(value)
199
+ case value
200
+ when nil then false
201
+ when Hash then value.any? { |_key, nested| reqcord_meaningful?(nested) }
202
+ when Array then value.any? { |item| reqcord_meaningful?(item) }
203
+ when String then !value.empty?
204
+ else true
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ module Capture
5
+ module MinitestContext
6
+ def before_setup
7
+ file, line = reqcord_source_location
8
+
9
+ TestContext.current = {
10
+ test: name,
11
+ suite: self.class.name,
12
+ file: file,
13
+ line: line
14
+ }
15
+
16
+ super
17
+ end
18
+
19
+ def after_teardown
20
+ super
21
+ ensure
22
+ TestContext.clear
23
+ end
24
+
25
+ private
26
+
27
+ def reqcord_source_location
28
+ method(name).source_location
29
+ rescue NameError
30
+ [nil, nil]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ module Capture
5
+ # The RSpec counterpart of MinitestContext: names captured examples after
6
+ # the request spec that produced them.
7
+ module RSpecContext
8
+ class << self
9
+ def install!(rspec = ::RSpec)
10
+ return false if @installed
11
+
12
+ rspec.configure do |config|
13
+ config.before(:each) do |example|
14
+ TestContext.current = RSpecContext.context_for(example)
15
+ end
16
+
17
+ config.after(:each) do
18
+ TestContext.clear
19
+ end
20
+ end
21
+
22
+ @installed = true
23
+ end
24
+
25
+ def installed?
26
+ @installed == true
27
+ end
28
+
29
+ def context_for(example)
30
+ metadata = example.metadata
31
+
32
+ {
33
+ test: metadata[:description],
34
+ suite: metadata[:example_group][:description],
35
+ file: metadata[:file_path],
36
+ line: metadata[:line_number]
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ module Capture
5
+ # The test that is currently running, kept per thread so parallel runners
6
+ # do not attribute an exchange to the wrong test.
7
+ module TestContext
8
+ THREAD_KEY = :reqcord_test_context
9
+
10
+ class << self
11
+ def current
12
+ Thread.current[THREAD_KEY] || {}
13
+ end
14
+
15
+ def current=(value)
16
+ Thread.current[THREAD_KEY] = value
17
+ end
18
+
19
+ def clear
20
+ Thread.current[THREAD_KEY] = nil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reqcord
4
+ # Capture runs inside the test process and writes to a file the generator
5
+ # reads afterwards. Both variables are set by `reqcord:generate`, so an
6
+ # ordinary test run patches nothing and writes nothing.
7
+ module Capture
8
+ class << self
9
+ def enabled?
10
+ ENV["REQCORD_CAPTURE"] == "1" &&
11
+ capture_file
12
+ end
13
+
14
+ def capture_file
15
+ ENV["REQCORD_CAPTURE_FILE"]
16
+ end
17
+ end
18
+ end
19
+ end