├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── rspec │ ├── request_describer.rb │ └── request_describer │ └── version.rb ├── rspec-request_describer.gemspec └── spec ├── rspec └── request_describer_spec.rb └── spec_helper.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | rspec: 11 | uses: r7kamura/workflows/.github/workflows/ruby-rspec.yml@main 12 | with: 13 | ruby-version: 2.7.4 14 | rubocop: 15 | uses: r7kamura/workflows/.github/workflows/ruby-rubocop.yml@main 16 | with: 17 | ruby-version: 2.7.4 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics: 2 | Enabled: false 3 | 4 | Style/Documentation: 5 | Enabled: false 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 0.6.0 - 2024-08-02 6 | 7 | ### Changed 8 | 9 | - Drop Ruby 2.6- support. 10 | 11 | ### Fixed 12 | 13 | - Reduce some files from gem package. 14 | - Correct some RuboCop offenses. 15 | 16 | ## 0.5.0 - 2024-03-28 17 | 18 | ### Added 19 | 20 | - Raise friendly error on incorrect `describe` usage. 21 | 22 | ## 0.4.0 - 2023-07-10 23 | 24 | ### Added 25 | 26 | - Support HEAD HTTP method. 27 | 28 | ## 0.3.2 - 2020-02-15 29 | 30 | ### Removed 31 | 32 | - Remove runtime gem dependency on actionpack. 33 | 34 | ## 0.3.1 - 2019-05-08 35 | 36 | ### Fixed 37 | 38 | - Fix env calculation timing. 39 | 40 | ## 0.3.0 - 2019-05-05 41 | 42 | ### Added 43 | 44 | - Support symbol keys in request headers. 45 | 46 | ## 0.2.2 - 2018-05-13 47 | 48 | ### Fixed 49 | 50 | - Fix bug: Ignore case-sensitivity of HTTP headers. 51 | 52 | ## 0.2.1 - 2017-02-27 53 | 54 | ### Fixed 55 | 56 | - Fix error from `#process`. 57 | 58 | ## 0.2.0 - 2017-02-27 59 | 60 | ### Added 61 | 62 | - Support actionpack 5.1.0. 63 | 64 | ### Changed 65 | 66 | - Declare runtime dependency on actionpack. 67 | 68 | ## 0.1.1 - 2016-05-15 69 | 70 | ### Fixed 71 | 72 | - Prevent warning for Rails 5. 73 | 74 | ## 0.1.0 - 2015-10-09 75 | 76 | ### Changed 77 | 78 | - Rename `method` with `http_method`. 79 | 80 | ## 0.0.9 - 2015-06-24 81 | 82 | ### Changed 83 | 84 | - Ignore case-sensivity on Content-Type checking. 85 | 86 | ## 0.0.8 - 2015-05-20 87 | 88 | ### Changed 89 | 90 | - Use more sophisticated method capture pattern. 91 | 92 | ## 0.0.7 - 2015-04-20 93 | 94 | ### Added 95 | 96 | - Add `send_request` to explicitly call `subject`. 97 | 98 | ## 0.0.6 - 2015-02-12 99 | 100 | ### Fixed 101 | 102 | - Allow any non-space characters in URL path. 103 | 104 | ## 0.0.5 - 2014-12-25 105 | 106 | ### Fixed 107 | 108 | - Allow hyphen in path. 109 | 110 | ## 0.0.4 - 2014-12-18 111 | 112 | ### Added 113 | 114 | - Define HTTPS as reserved header name. 115 | 116 | ## 0.0.3 - 2014-10-13 117 | 118 | ### Removed 119 | 120 | - Remove dependency on ActiveSupport's `Object#in?`. 121 | 122 | ## 0.0.2 - 2014-09-25 123 | 124 | ### Added 125 | 126 | - Support RSpec 3. 127 | 128 | ## 0.0.1 - 2014-08-29 129 | 130 | ### Added 131 | 132 | - 1st Release. 133 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in rspec-request_describer.gemspec 6 | gemspec 7 | 8 | gem 'rake' 9 | gem 'rspec' 10 | gem 'rubocop' 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rspec-request_describer (0.6.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | diff-lcs (1.5.1) 11 | json (2.7.2) 12 | language_server-protocol (3.17.0.3) 13 | parallel (1.25.1) 14 | parser (3.3.4.0) 15 | ast (~> 2.4.1) 16 | racc 17 | racc (1.8.1) 18 | rainbow (3.1.1) 19 | rake (13.2.1) 20 | regexp_parser (2.9.2) 21 | rexml (3.3.9) 22 | rspec (3.13.0) 23 | rspec-core (~> 3.13.0) 24 | rspec-expectations (~> 3.13.0) 25 | rspec-mocks (~> 3.13.0) 26 | rspec-core (3.13.0) 27 | rspec-support (~> 3.13.0) 28 | rspec-expectations (3.13.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.13.0) 31 | rspec-mocks (3.13.1) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.13.0) 34 | rspec-support (3.13.1) 35 | rubocop (1.65.1) 36 | json (~> 2.3) 37 | language_server-protocol (>= 3.17.0) 38 | parallel (~> 1.10) 39 | parser (>= 3.3.0.2) 40 | rainbow (>= 2.2.2, < 4.0) 41 | regexp_parser (>= 2.4, < 3.0) 42 | rexml (>= 3.2.5, < 4.0) 43 | rubocop-ast (>= 1.31.1, < 2.0) 44 | ruby-progressbar (~> 1.7) 45 | unicode-display_width (>= 2.4.0, < 3.0) 46 | rubocop-ast (1.31.3) 47 | parser (>= 3.3.1.0) 48 | ruby-progressbar (1.13.0) 49 | unicode-display_width (2.5.0) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | rake 56 | rspec 57 | rspec-request_describer! 58 | rubocop 59 | 60 | BUNDLED WITH 61 | 2.2.29 62 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ryo Nakamura 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSpec::RequestDescriber 2 | 3 | [![test](https://github.com/r7kamura/rspec-request_describer/actions/workflows/test.yml/badge.svg)](https://github.com/r7kamura/rspec-request_describer/actions/workflows/test.yml) 4 | [![Gem Version](https://badge.fury.io/rb/rspec-request_describer.svg)](https://rubygems.org/gems/rspec-request_describer) 5 | 6 | RSpec plugin to write self-documenting request-specs. 7 | 8 | This gem is designed for: 9 | 10 | - [rack-test](https://github.com/rack-test/rack-test) 11 | - [rspec-rails](https://github.com/rspec/rspec-rails) 12 | 13 | ## Setup 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | group :test do 19 | gem 'rspec-request_describer' 20 | end 21 | ``` 22 | 23 | And then include `RSpec::RequestDescriber`: 24 | 25 | ```ruby 26 | # spec/rails_helper.rb 27 | RSpec.configure do |config| 28 | config.include RSpec::RequestDescriber, type: :request 29 | end 30 | ``` 31 | 32 | ## Usage 33 | 34 | Write HTTP method and URL path in the top-level description of your request-specs. 35 | 36 | ```ruby 37 | # spec/requests/users/index_spec.rb 38 | RSpec.describe 'GET /users' do 39 | it 'returns 200' do 40 | subject 41 | expect(response).to have_http_status(200) 42 | end 43 | end 44 | ``` 45 | 46 | Internally, `RSpec::RequestDescriber` defines `subject` and some `let` from its top-level description like this: 47 | 48 | ```ruby 49 | RSpec.describe 'GET /users' do 50 | subject do 51 | __send__(http_method, path, headers:, params:) 52 | end 53 | 54 | let(:http_method) do 55 | 'get' 56 | end 57 | 58 | let(:path) do 59 | '/users' 60 | end 61 | 62 | let(:headers) do 63 | {} 64 | end 65 | 66 | let(:params) do 67 | {} 68 | end 69 | 70 | it 'returns 200' do 71 | subject 72 | expect(response).to have_http_status(200) 73 | end 74 | end 75 | ``` 76 | 77 | ### headers 78 | 79 | If you want to modify request headers, change `headers`: 80 | 81 | ```ruby 82 | RSpec.describe 'GET /users' do 83 | context 'with Authorization header' do 84 | before do 85 | headers['Authorization'] = 'token 12345' 86 | end 87 | 88 | it 'returns 200' do 89 | subject 90 | expect(response).to have_http_status(200) 91 | end 92 | end 93 | end 94 | ``` 95 | 96 | ### params 97 | 98 | If you want to modify request parameters, change `params`: 99 | 100 | ```ruby 101 | RSpec.describe 'GET /users' do 102 | context 'with sort parameter' do 103 | before do 104 | params['sort'] = 'id' 105 | end 106 | 107 | it 'returns 200 with expected JSON body' do 108 | subject 109 | expect(response).to have_http_status(200) 110 | expect(response.parsed_body).to match( 111 | [ 112 | hash_including('id' => 1), 113 | hash_including('id' => 2), 114 | ] 115 | ) 116 | end 117 | end 118 | end 119 | ``` 120 | 121 | ### path parameters 122 | 123 | You can embed variables in URL path like `/users/:user_id`. 124 | In this example, the returned value from `#user_id` method will be embedded as its real value. 125 | 126 | ```ruby 127 | RSpec.describe 'GET /users/:user_id' do 128 | let(:user) do 129 | User.create(name: 'alice') 130 | end 131 | 132 | let(:user_id) do 133 | user.id 134 | end 135 | 136 | it 'returns 200' do 137 | subject 138 | expect(response).to have_http_status(200) 139 | end 140 | end 141 | ``` 142 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | -------------------------------------------------------------------------------- /lib/rspec/request_describer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/request_describer/version' 4 | 5 | module RSpec 6 | module RequestDescriber 7 | class IncorrectDescribe < StandardError; end 8 | 9 | RESERVED_HEADER_NAMES = %w[ 10 | content-type 11 | host 12 | https 13 | ].freeze 14 | 15 | SUPPORTED_METHODS = %w[ 16 | DELETE 17 | GET 18 | HEAD 19 | PATCH 20 | POST 21 | PUT 22 | ].freeze 23 | 24 | class << self 25 | def included(base) 26 | base.instance_eval do 27 | subject do 28 | send_request 29 | end 30 | 31 | let(:send_request) do 32 | send( 33 | http_method, 34 | path, 35 | headers: env, 36 | params: request_body 37 | ) 38 | end 39 | 40 | let(:request_body) do 41 | if headers.any? { |key, value| key.to_s.casecmp('content-type').zero? && value == 'application/json' } 42 | params.to_json 43 | else 44 | params.inject({}) do |result, (key, value)| 45 | result.merge(key.to_s => value) 46 | end 47 | end 48 | end 49 | 50 | let(:headers) do 51 | {} 52 | end 53 | 54 | let(:params) do 55 | {} 56 | end 57 | 58 | let(:env) do 59 | headers.inject({}) do |result, (key, value)| 60 | key = key.to_s 61 | key = "HTTP_#{key}" unless RESERVED_HEADER_NAMES.include?(key.downcase) 62 | key = key.tr('-', '_').upcase 63 | result.merge(key => value) 64 | end 65 | end 66 | 67 | let(:endpoint_segments) do 68 | current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example 69 | match = current_example.full_description.match(/(#{::Regexp.union(SUPPORTED_METHODS)}) (\S+)/) 70 | raise IncorrectDescribe, 'Please use the format "METHOD /path" for the describe' unless match 71 | 72 | match.to_a 73 | end 74 | 75 | # @return [String] e.g. `"get"` 76 | let(:http_method) do 77 | endpoint_segments[1].downcase 78 | end 79 | 80 | let(:path) do 81 | endpoint_segments[2].gsub(/:(\w+[!?=]?)/) { send(Regexp.last_match(1)) } 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/rspec/request_describer/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module RequestDescriber 5 | VERSION = '0.6.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /rspec-request_describer.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'rspec/request_describer/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'rspec-request_describer' 9 | spec.version = RSpec::RequestDescriber::VERSION 10 | spec.authors = ['Ryo Nakamura'] 11 | spec.email = ['r7kamura@gmail.com'] 12 | spec.summary = 'An RSpec plugin to write self-documenting request-specs.' 13 | spec.homepage = 'https://github.com/r7kamura/rspec-request_describer' 14 | spec.license = 'MIT' 15 | 16 | spec.required_ruby_version = '>= 2.7' 17 | 18 | gemspec = File.basename(__FILE__) 19 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 20 | ls.readlines("\x0", chomp: true).reject do |f| 21 | (f == gemspec) || 22 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) 23 | end 24 | end 25 | 26 | spec.require_paths = ['lib'] 27 | end 28 | -------------------------------------------------------------------------------- /spec/rspec/request_describer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | 5 | RSpec.describe RSpec::RequestDescriber do 6 | include RSpec::RequestDescriber 7 | 8 | def get(*args) 9 | [__method__, *args] 10 | end 11 | 12 | describe 'GET /users' do 13 | it 'calls #get' do 14 | is_expected.to eq( 15 | [ 16 | :get, 17 | '/users', 18 | { headers: {}, 19 | params: {} } 20 | ] 21 | ) 22 | end 23 | 24 | context 'with headers' do 25 | let(:headers) do 26 | super().merge('Authorization' => 'token 12345') 27 | end 28 | 29 | it 'calls #get with HTTP_ prefixed and upper-cased headers' do 30 | is_expected.to eq( 31 | [ 32 | :get, 33 | '/users', 34 | { headers: { 'HTTP_AUTHORIZATION' => 'token 12345' }, 35 | params: {} } 36 | ] 37 | ) 38 | end 39 | end 40 | 41 | context 'with headers including reserved header name' do 42 | let(:headers) do 43 | super().merge('Https' => 'on') 44 | end 45 | 46 | it 'calls #get with headers with non HTTP_ prefixed and upper-cased headers' do 47 | is_expected.to eq( 48 | [ 49 | :get, 50 | '/users', 51 | { headers: { 'HTTPS' => 'on' }, 52 | params: {} } 53 | ] 54 | ) 55 | end 56 | end 57 | 58 | context 'with symbolized keys headers' do 59 | let(:headers) do 60 | super().merge(AUTHORIZATION: 'token 12345') 61 | end 62 | 63 | it 'calls #get with HTTP_ prefixed and stringified keys headers' do 64 | is_expected.to eq( 65 | [ 66 | :get, 67 | '/users', 68 | { headers: { 'HTTP_AUTHORIZATION' => 'token 12345' }, 69 | params: {} } 70 | ] 71 | ) 72 | end 73 | end 74 | 75 | context 'with headers including request body' do 76 | before do 77 | headers['X-Signature'] = "sha1=#{OpenSSL::HMAC.hexdigest('SHA1', 'secret', request_body.to_s)}" 78 | end 79 | 80 | it 'calls #get with HTTP_ prefixed and stringified keys headers' do 81 | is_expected.to eq( 82 | [ 83 | :get, 84 | '/users', 85 | { headers: { 'HTTP_X_SIGNATURE' => 'sha1=5d61605c3feea9799210ddcb71307d4ba264225f' }, 86 | params: {} } 87 | ] 88 | ) 89 | end 90 | end 91 | 92 | context 'with params' do 93 | let(:params) do 94 | super().merge('sort' => 'id') 95 | end 96 | 97 | it 'calls #get with params' do 98 | is_expected.to eq( 99 | [ 100 | :get, 101 | '/users', 102 | { headers: {}, 103 | params: { 'sort' => 'id' } } 104 | ] 105 | ) 106 | end 107 | end 108 | 109 | context 'with symbolized keys params' do 110 | let(:params) do 111 | super().merge(sort: 'id') 112 | end 113 | 114 | it 'calls #get with stringified keys params' do 115 | is_expected.to eq( 116 | [ 117 | :get, 118 | '/users', 119 | { headers: {}, 120 | params: { 'sort' => 'id' } } 121 | ] 122 | ) 123 | end 124 | end 125 | end 126 | 127 | describe 'GET /users/:user_id' do 128 | let(:user_id) do 129 | 1 130 | end 131 | 132 | it 'calles #get with embeded variable in URL path' do 133 | is_expected.to eq( 134 | [ 135 | :get, 136 | '/users/1', 137 | { headers: {}, 138 | params: {} } 139 | ] 140 | ) 141 | end 142 | end 143 | 144 | context 'when the test case is under the top-level describe unexpectedly' do 145 | it 'handles the error' do 146 | expect { subject }.to raise_error(RSpec::RequestDescriber::IncorrectDescribe) 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/request_describer' 4 | 5 | RSpec.configure do |config| 6 | config.expect_with :rspec do |expectations| 7 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 8 | end 9 | 10 | config.mock_with :rspec do |mocks| 11 | mocks.verify_partial_doubles = true 12 | end 13 | 14 | config.shared_context_metadata_behavior = :apply_to_host_groups 15 | 16 | config.filter_run_when_matching :focus 17 | 18 | config.disable_monkey_patching! 19 | 20 | config.warnings = true 21 | 22 | config.default_formatter = 'doc' if config.files_to_run.one? 23 | end 24 | --------------------------------------------------------------------------------