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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/CHANGELOG.md +77 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +171 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -95
- data/Rakefile +13 -0
- data/docs/configuration.md +319 -0
- data/examples/complex-spec-app/.rspec +1 -0
- data/examples/complex-spec-app/README.md +21 -0
- data/examples/complex-spec-app/app.rb +363 -0
- data/examples/complex-spec-app/docs/api/README.md +104 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/create.md +117 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/destroy.md +51 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/index.md +11 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/list.md +106 -0
- data/examples/complex-spec-app/docs/api/api/v1/auth/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v1/auth/login.md +76 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/create.md +91 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/destroy.md +51 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/checkout.md +102 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/show.md +73 -0
- data/examples/complex-spec-app/docs/api/api/v1/home/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v1/home/list.md +47 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/create.md +94 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/list.md +61 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/cancel.md +69 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/create.md +150 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/destroy.md +37 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/index.md +13 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/list.md +78 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/show.md +83 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/index.md +11 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/list.md +83 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/search.md +88 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/show.md +71 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/show.md +61 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/update.md +91 -0
- data/examples/complex-spec-app/docs/api/api/v2/products/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v2/products/list.md +77 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/auth/login.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/carts/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/home/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/notes/create.sh +15 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/notes/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/create.sh +29 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/search.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/show.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
- data/examples/complex-spec-app/docs/api/curl/api/v2/products/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/dataset.json +5116 -0
- data/examples/complex-spec-app/docs/api/postman/collection.json +2519 -0
- data/examples/complex-spec-app/generate.rb +14 -0
- data/examples/complex-spec-app/reqcord.yml +28 -0
- data/examples/complex-spec-app/spec/requests/admin_spec.rb +67 -0
- data/examples/complex-spec-app/spec/requests/cart_spec.rb +61 -0
- data/examples/complex-spec-app/spec/requests/home_and_auth_spec.rb +49 -0
- data/examples/complex-spec-app/spec/requests/orders_spec.rb +124 -0
- data/examples/complex-spec-app/spec/requests/products_spec.rb +65 -0
- data/examples/complex-spec-app/spec/spec_helper.rb +17 -0
- data/examples/complex-test-app/README.md +40 -0
- data/examples/complex-test-app/app.rb +384 -0
- data/examples/complex-test-app/docs/api/README.md +104 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/create.md +117 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/destroy.md +51 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/index.md +11 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/list.md +106 -0
- data/examples/complex-test-app/docs/api/api/v1/auth/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v1/auth/login.md +76 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/create.md +91 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/destroy.md +51 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/checkout.md +102 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/show.md +73 -0
- data/examples/complex-test-app/docs/api/api/v1/home/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v1/home/list.md +47 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/create.md +94 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/list.md +61 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/cancel.md +69 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/create.md +150 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/destroy.md +37 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/index.md +13 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/list.md +71 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/show.md +83 -0
- data/examples/complex-test-app/docs/api/api/v1/products/index.md +11 -0
- data/examples/complex-test-app/docs/api/api/v1/products/list.md +83 -0
- data/examples/complex-test-app/docs/api/api/v1/products/search.md +88 -0
- data/examples/complex-test-app/docs/api/api/v1/products/show.md +71 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/show.md +61 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/update.md +91 -0
- data/examples/complex-test-app/docs/api/api/v2/products/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v2/products/list.md +77 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/auth/login.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/carts/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/home/list.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/notes/create.sh +15 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/notes/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/create.sh +29 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/list.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/search.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/show.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
- data/examples/complex-test-app/docs/api/curl/api/v2/products/list.sh +8 -0
- data/examples/complex-test-app/docs/api/dataset.json +5109 -0
- data/examples/complex-test-app/docs/api/postman/collection.json +2568 -0
- data/examples/complex-test-app/generate.rb +14 -0
- data/examples/complex-test-app/reqcord.yml +28 -0
- data/examples/complex-test-app/test/integration/admin_test.rb +70 -0
- data/examples/complex-test-app/test/integration/cart_test.rb +64 -0
- data/examples/complex-test-app/test/integration/home_and_auth_test.rb +53 -0
- data/examples/complex-test-app/test/integration/orders_test.rb +127 -0
- data/examples/complex-test-app/test/integration/products_test.rb +66 -0
- data/examples/reqcord.yml +58 -0
- data/examples/spec-app/.rspec +1 -0
- data/examples/spec-app/README.md +36 -0
- data/examples/spec-app/app.rb +209 -0
- data/examples/spec-app/docs/api/README.md +45 -0
- data/examples/spec-app/docs/api/api/v1/customers/create.md +95 -0
- data/examples/spec-app/docs/api/api/v1/customers/index.md +11 -0
- data/examples/spec-app/docs/api/api/v1/customers/list.md +67 -0
- data/examples/spec-app/docs/api/api/v1/customers/show.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/complete.md +51 -0
- data/examples/spec-app/docs/api/api/v1/tasks/create.md +90 -0
- data/examples/spec-app/docs/api/api/v1/tasks/destroy.md +35 -0
- data/examples/spec-app/docs/api/api/v1/tasks/index.md +14 -0
- data/examples/spec-app/docs/api/api/v1/tasks/list.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/show.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/update.md +74 -0
- data/examples/spec-app/docs/api/api/v1/users/create.md +95 -0
- data/examples/spec-app/docs/api/api/v1/users/index.md +11 -0
- data/examples/spec-app/docs/api/api/v1/users/list.md +67 -0
- data/examples/spec-app/docs/api/api/v1/users/show.md +65 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/create.sh +17 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/list.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/show.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/create.sh +17 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/list.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/show.sh +9 -0
- data/examples/spec-app/docs/api/dataset.json +2402 -0
- data/examples/spec-app/docs/api/postman/collection.json +1290 -0
- data/examples/spec-app/generate.rb +14 -0
- data/examples/spec-app/reqcord.yml +27 -0
- data/examples/spec-app/spec/requests/customers_spec.rb +71 -0
- data/examples/spec-app/spec/requests/tasks_spec.rb +78 -0
- data/examples/spec-app/spec/requests/users_spec.rb +68 -0
- data/examples/spec-app/spec/spec_helper.rb +17 -0
- data/examples/test-app/README.md +57 -0
- data/examples/test-app/app.rb +212 -0
- data/examples/test-app/docs/api/README.md +45 -0
- data/examples/test-app/docs/api/api/v1/customers/create.md +95 -0
- data/examples/test-app/docs/api/api/v1/customers/index.md +11 -0
- data/examples/test-app/docs/api/api/v1/customers/list.md +67 -0
- data/examples/test-app/docs/api/api/v1/customers/show.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/complete.md +51 -0
- data/examples/test-app/docs/api/api/v1/tasks/create.md +90 -0
- data/examples/test-app/docs/api/api/v1/tasks/destroy.md +35 -0
- data/examples/test-app/docs/api/api/v1/tasks/index.md +14 -0
- data/examples/test-app/docs/api/api/v1/tasks/list.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/show.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/update.md +74 -0
- data/examples/test-app/docs/api/api/v1/users/create.md +95 -0
- data/examples/test-app/docs/api/api/v1/users/index.md +11 -0
- data/examples/test-app/docs/api/api/v1/users/list.md +67 -0
- data/examples/test-app/docs/api/api/v1/users/show.md +65 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/create.sh +17 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/list.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/show.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
- data/examples/test-app/docs/api/curl/api/v1/users/create.sh +17 -0
- data/examples/test-app/docs/api/curl/api/v1/users/list.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/users/show.sh +9 -0
- data/examples/test-app/docs/api/dataset.json +2402 -0
- data/examples/test-app/docs/api/postman/collection.json +1290 -0
- data/examples/test-app/generate.rb +14 -0
- data/examples/test-app/reqcord.yml +28 -0
- data/examples/test-app/test/integration/customers_test.rb +72 -0
- data/examples/test-app/test/integration/tasks_test.rb +80 -0
- data/examples/test-app/test/integration/users_test.rb +72 -0
- data/lib/reqcord/capture/collector.rb +31 -0
- data/lib/reqcord/capture/integration_patch.rb +209 -0
- data/lib/reqcord/capture/minitest_context.rb +34 -0
- data/lib/reqcord/capture/rspec_context.rb +42 -0
- data/lib/reqcord/capture/test_context.rb +25 -0
- data/lib/reqcord/capture.rb +19 -0
- data/lib/reqcord/configuration.rb +198 -0
- data/lib/reqcord/dataset.rb +176 -0
- data/lib/reqcord/endpoint.rb +263 -0
- data/lib/reqcord/errors.rb +9 -0
- data/lib/reqcord/exporters/curl.rb +68 -0
- data/lib/reqcord/exporters/markdown.rb +295 -0
- data/lib/reqcord/exporters/postman.rb +206 -0
- data/lib/reqcord/exporters.rb +32 -0
- data/lib/reqcord/generator.rb +364 -0
- data/lib/reqcord/railtie.rb +51 -0
- data/lib/reqcord/renderers/curl.rb +56 -0
- data/lib/reqcord/renderers/payload.rb +69 -0
- data/lib/reqcord/request_example.rb +104 -0
- data/lib/reqcord/response_example.rb +72 -0
- data/lib/reqcord/route_collector.rb +242 -0
- data/lib/reqcord/sanitizers/sanitizer.rb +140 -0
- data/lib/reqcord/schema.rb +187 -0
- data/lib/reqcord/support.rb +58 -0
- data/lib/reqcord/version.rb +5 -0
- data/lib/reqcord.rb +78 -0
- data/lib/tasks/reqcord.rake +99 -0
- data/reqcord.gemspec +46 -0
- 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
|