rspec-api 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f66889873ee973973a2f419e950bd9f405abe7e2
4
- data.tar.gz: bbd9a7c7f60d30eaad0e46a28de310e538933bc3
3
+ metadata.gz: 9ca5599e96ba26d321698c09c88a338919c4bf8a
4
+ data.tar.gz: ae0a49475f6343996a3eb2313ecba068e20db34b
5
5
  SHA512:
6
- metadata.gz: 0bfaa8bfff6796c6620802a92505c7c4d89c29bfac3283123ab8d9aec942bb8805c02030434646898f119a36f817ca838b4182fcf776cfe02484af0e624a138e
7
- data.tar.gz: 6eb04e4cd39cbf6275a1a8ce4e850996e3d27417bdd004947830ca1a11d484633fcb742636ea9c25f3d7f571b634b72efabc4f69a4dc0b4029c8dfa9a3e4ebb1
6
+ metadata.gz: c26cdc3ffe743a93ee67be9567a2fcbf70810f54381c866b971626cd4a5961828fcfb56e0dceddeeea3e5a0b5ab08119a8aefb787262f7df3c9bc0aa8aacf4f1
7
+ data.tar.gz: 26b169c8bbaa0d2e9893b764bcfadcf9a187942a312dc6ca2fb17cb41391b4d153c1f65e94d74063a77a32b8249fe476619874b4f8b665c939a45337af8c4f95
data/lib/rspec-api.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rspec-api/dsl'
data/lib/rspec-api/dsl.rb CHANGED
@@ -1,24 +1,45 @@
1
- require 'rspec-api/dsl/resource'
2
- require 'rspec-api/dsl/route'
3
- require 'rspec-api/dsl/request'
1
+ require 'rspec-api/dsl/accepts'
2
+ require 'rspec-api/dsl/actions'
3
+ require 'rspec-api/dsl/attributes'
4
4
 
5
- module DSL
5
+ module RSpecApi
6
+ module DSL
7
+ include Accepts
8
+ include Actions
9
+ include Attributes
10
+ include Fixtures
11
+ end
6
12
  end
7
13
 
8
- # Just like RSpec Core’s `describe` method, `resource` generates a subclass of
9
- # {ExampleGroup} and is available at the top-level namespace.
10
- # The difference is that examples declared inside a `resource` block have access
11
- # to RSpec API own methods, defined in DSL::Resource, such as `has_attribute`,
12
- # `accepts_filter`, `get`, `post`, and so on.
13
- def resource(name, args = {}, &block)
14
- args.merge! rspec_api_dsl: :resource, rspec_api: {resource_name: name}
15
- describe name, args, &block
16
- end
14
+ # RSpecApi provides methods to test RESTful APIs.
15
+ #
16
+ # To have these methods available in your RSpec ExampleGroups you have to
17
+ # tag the `describe` block with the `:rspec_api` metadata.
18
+ #
19
+ # describe "Artists", rspec_api: true do
20
+ # ... # here you can use `get`, `delete`, etc.
21
+ # end
22
+ RSpec.configuration.extend RSpecApi::DSL, rspec_api: true
17
23
 
18
- RSpec.configuration.include DSL::Resource, rspec_api_dsl: :resource
19
- RSpec.configuration.include DSL::Route, rspec_api_dsl: :route
20
- RSpec.configuration.include DSL::Request, rspec_api_dsl: :request
21
-
22
- if RSpec::Core::Version::STRING >= '2.14'
23
- RSpec.configuration.backtrace_exclusion_patterns << %r{lib/rspec-api/dsl\.rb}
24
+ # Alternatively, you replace `describe` with `resource`, for the same result:
25
+ #
26
+ #
27
+ # resource :artist do
28
+ # ... # same as before, here you can use `get`, `delete`, etc.
29
+ # end
30
+ #
31
+ #
32
+ # `resource` is indeed the only top-level namespace RSpecApi method.
33
+ def resource(name, args = {}, &block)
34
+ args.merge! rspec_api: true
35
+ resources = (@resources ||= [])
36
+ resource = describe name.to_s.pluralize.humanize, args do
37
+ @resource = name
38
+ @resources = resources
39
+ @expectations = []
40
+ @expectations << {query_params: {}, status_expect: {status: 200}, body_expect: {attributes: {name: {type: :string}, website: {type: [:null, string: :url]}}}}
41
+ @attributes = {}
42
+ instance_exec &block
43
+ end
44
+ @resources << resource
24
45
  end
@@ -0,0 +1,46 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module Accepts
4
+
5
+ def accepts_page(page_parameter)
6
+ any_page = 2
7
+ @expectations << {
8
+ query_params: {}.tap{|qp| qp[page_parameter] = any_page},
9
+ status_expect: {status: 200},
10
+ headers_expect: {has_prev_page: true}
11
+ }
12
+ end
13
+
14
+ def accepts_sort(sort_parameter, options={})
15
+ @expectations << {
16
+ query_params: {sort: sort_parameter}.merge(options.fetch(:sort_if, {})),
17
+ before: create_fixture,
18
+ after: destroy_fixture,
19
+ status_expect: {status: 200},
20
+ body_expect: {sort: options.slice(:by, :verse)}
21
+ }
22
+ end
23
+
24
+ def accepts_callback(callback_parameter)
25
+ any_callback = 'a_callback'
26
+ @expectations << {
27
+ query_params: {}.tap{|qp| qp[callback_parameter] = any_callback},
28
+ status_expect: {status: 200},
29
+ body_expect: {callback: any_callback}
30
+ }
31
+ end
32
+
33
+ def accepts_filter(filter_parameter, options={})
34
+ value = existing(options[:by])
35
+ @expectations << {
36
+ query_params: {}.tap{|qp| qp[filter_parameter] = value},
37
+ before: create_fixture,
38
+ after: destroy_fixture,
39
+ status_expect: {status: 200},
40
+ body_expect: {filter: options.slice(:by, :comparing_with).merge(value: value)}
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,36 @@
1
+ require 'rspec-api/dsl/requests'
2
+
3
+ module RSpecApi
4
+ module DSL
5
+ module Actions
6
+ include RSpecApi::DSL::Fixtures
7
+ include RSpecApi::DSL::Requests
8
+
9
+ def self.define_action(action)
10
+ define_method action do |route, options = {}, &block|
11
+ expectations = @expectations
12
+ attributes = @attributes
13
+ authorization = @authorization
14
+ resource = @resource
15
+ collection = options[:collection]
16
+ describe "#{action.upcase} #{route}" do
17
+ @action = action
18
+ @route = route
19
+ @expectations = expectations
20
+ @attributes = attributes
21
+ @authorization = authorization
22
+ @collection = collection
23
+ @resource = resource
24
+ instance_eval &block
25
+ end
26
+ end
27
+ end
28
+
29
+ define_action :get
30
+ define_action :put
31
+ define_action :patch
32
+ define_action :post
33
+ define_action :delete
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module Attributes
4
+ def has_attribute(name, options = {}, &block)
5
+ @attributes[name] = options
6
+ end
7
+
8
+ def attributes_of(resource)
9
+ @resources.find{|r| r.description == resource.to_s.pluralize.humanize}.instance_variable_get '@attributes'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,78 @@
1
+ require 'rspec-api/dsl/responses'
2
+
3
+ module RSpecApi
4
+ module DSL
5
+ module Requests
6
+ def my_collection_request(query_params = {}, &block)
7
+ @expectations.each do |options|
8
+ single_request(options.deep_merge(body_expect: {collection: true}, query_params: query_params), &block)
9
+ end
10
+ end
11
+
12
+ def respond_with(*args) # NOT THE TRUE ONE
13
+ request_with do
14
+ respond_with(*args)
15
+ end
16
+ end
17
+
18
+ def request_with(query_params = {}, &block)
19
+ if @collection
20
+ my_collection_request(query_params, &block)
21
+ else
22
+ single_request(query_params: query_params, &block)
23
+ end
24
+ end
25
+
26
+ def single_request(options = {}, &block)
27
+ query_params = options.fetch(:query_params, {})
28
+ status_expect = options.fetch(:status_expect, {status: 200})
29
+ headers_expect = options.fetch(:headers_expect, {type: :json})
30
+ body_expect = options.fetch(:body_expect, {collection: false}) # Or maybe nothing
31
+ body_expect = body_expect.merge(attributes: @attributes) if @attributes
32
+ before_fun = options.fetch(:before, -> {})
33
+ after_fun = options.fetch(:after, -> {})
34
+ query = {authorization: @authorization, route_params: {}, action: @action, route: @route, query_params: query_params, status_expect: status_expect, headers_expect: headers_expect, body_expect: body_expect, before: before_fun, after: after_fun}
35
+
36
+ query_params.each do |k, v|
37
+ if v.is_a?(Hash) && v[:proc] && v[:value] # so we avoid getting the hash of the callback called :proc
38
+ query[:query_params][k] = v[:value]
39
+ if v[:proc] == :existing
40
+ query = query.merge before: create_fixture, after: destroy_fixture
41
+ end
42
+ end
43
+ end
44
+
45
+ my_real_request(query, &block)
46
+ end
47
+
48
+ def my_real_request(query = {}, &block)
49
+ query[:before].call
50
+
51
+ query.fetch(:query_params, {}).each do |k, v|
52
+ query[:query_params][k] = v.is_a?(Proc) ? v.call : v
53
+ if query[:route].match "/:#{k}"
54
+ query[:route] = query[:route].gsub "/:#{k}", "/#{query[:query_params][k]}"
55
+ query[:route_params][k] = query[:query_params].delete k
56
+ end
57
+ end
58
+
59
+ describe "with #{query[:route].is_a?(Proc) ? query[:route].call : query[:route]}#{" and #{query[:query_params]}" if query[:query_params].any?}" do
60
+ extend RSpecApi::DSL::Responses
61
+ extend RSpecApi::DSL::HttpClient
62
+ send_request query[:action], (query[:route].is_a?(Proc) ? query[:route].call : query[:route]), query[:query_params], query[:authorization]
63
+ @response = last_response
64
+ @status_expect = query.fetch :status_expect, {}
65
+ @headers_expect = query.fetch :headers_expect, {}
66
+ @body_expect = query.fetch :body_expect, {}
67
+ @route_params = query.fetch :route_params, {}
68
+ if @body_expect.fetch(:filter, {})[:value].is_a?(Hash) && @body_expect.fetch(:filter, {})[:value][:proc]
69
+ @body_expect[:filter][:value] = @body_expect[:filter][:value][:value].call
70
+ end
71
+ instance_exec &block
72
+ end
73
+
74
+ query[:after].call
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,37 @@
1
+ require 'rspec-api/expectations'
2
+
3
+ module RSpecApi
4
+ module DSL
5
+ module Responses
6
+ include RSpecApi::Expectations::Resourceful
7
+
8
+ def respond_with(*args, &block)
9
+ if args.first.is_a?(Hash)
10
+ more_status_expect = args.first
11
+ more_headers_expect = {}
12
+ more_body_expect = {}
13
+ elsif args.first.is_a?(Array)
14
+ more_status_expect, more_headers_expect, more_body_expect = args
15
+ else
16
+ more_status_expect = {status: args.first}
17
+ more_headers_expect = {}
18
+ more_body_expect = {}
19
+ end
20
+
21
+ all_expectations = @status_expect.merge(more_status_expect).merge(
22
+ @headers_expect).merge(more_headers_expect).merge(@body_expect).
23
+ merge(more_body_expect)
24
+
25
+ expect_resourceful(@response, all_expectations)
26
+ expect_custom(@response, @route_params, &block) if block_given?
27
+ end
28
+
29
+ def expect_custom(response, route_params, &block)
30
+ context 'matches custom expectations' do
31
+ # THE ONLY MISSING THING:
32
+ it { instance_exec response, route_params, &block }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module Fixtures # rename to Fixtures
4
+ def existing(field)
5
+ # To be overriden
6
+ end
7
+
8
+ def unknown(field)
9
+ # To be overriden
10
+ end
11
+
12
+ def apply(method_name, options = {})
13
+ options[:to].merge(apply: method_name, value: -> { options[:to][:value].call.send method_name })
14
+ end
15
+
16
+ def valid(options = {})
17
+ # TODO: Here change the description
18
+ options
19
+ end
20
+
21
+ def invalid(options = {})
22
+ # TODO: Here change the description
23
+ options
24
+ end
25
+
26
+ def create_fixture
27
+ # To be overriden
28
+ end
29
+
30
+ def destroy_fixture
31
+ # To be overriden
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,50 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module Fixtures # rename to Fixtures
4
+ def existing(field)
5
+ {field: field, proc: :existing, value: existing_value_for(field)}
6
+ end
7
+
8
+ def unknown(field)
9
+ {field: field, proc: :unknown, value: missing_value_for(field)}
10
+ end
11
+
12
+ def apply(method_name, options = {})
13
+ options[:to].merge(apply: method_name, value: -> { options[:to][:value].call.send method_name })
14
+ end
15
+
16
+ def valid(options = {})
17
+ # TODO: Here change the description
18
+ options
19
+ end
20
+
21
+ def invalid(options = {})
22
+ # TODO: Here change the description
23
+ options
24
+ end
25
+
26
+ def create_fixture
27
+ # TODO: Random values from attributes
28
+ -> {
29
+ model = @resource.to_s.classify.constantize
30
+ case @resource
31
+ when :artist then model.create! name: 'Madonna', website: 'http://www.example.com'
32
+ when :concert then model.create! where: 'Coachella', year: 2010
33
+ end
34
+ }
35
+ end
36
+
37
+ def destroy_fixture
38
+ -> {@resource.to_s.classify.constantize.destroy_all}
39
+ end
40
+
41
+ def existing_value_for(field)
42
+ -> {@resource.to_s.classify.constantize.pluck(field).first}
43
+ end
44
+
45
+ def missing_value_for(field)
46
+ -> {-1}
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,82 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module Fixtures # rename to Fixtures
4
+ def existing(field)
5
+ {field: field, proc: :existing, value: existing_value_for(field)}
6
+ end
7
+
8
+ def unknown(field)
9
+ {field: field, proc: :unknown, value: missing_value_for(field)}
10
+ end
11
+
12
+ def apply(method_name, options = {})
13
+ options[:to].merge(apply: method_name, value: -> { options[:to][:value].call.send method_name })
14
+ end
15
+
16
+ def valid(options = {})
17
+ # TODO: Here change the description
18
+ options
19
+ end
20
+
21
+ def invalid(options = {})
22
+ # TODO: Here change the description
23
+ options
24
+ end
25
+
26
+ def create_fixture
27
+ # TODO: Nothing for now
28
+ -> { }
29
+ end
30
+
31
+ def destroy_fixture
32
+ # TODO: Nothing for now
33
+ -> { }
34
+ end
35
+
36
+ def existing_value_for(field)
37
+ value = case field
38
+ when :org then 'rspec-api'
39
+ when :user, :owner, :assignee then 'rspecapi'
40
+ when :repo then 'guinea-pig' # has heads, tails, pull, notes
41
+ when :gist_id then '7175672'
42
+ when :gist_comment_id then '937901'
43
+
44
+ when :blob_sha then 'f32932f7c927d86f57f56d703ac2ed100ceb0e47'
45
+ when :commit_sha then 'c98a37ea3b2759d0c43fb8abfa9abd3146938790'
46
+ when :tree_sha then 'ebca91692290192f50acc307af9fe26b2eab4274'
47
+ when :ref then 'heads/master'
48
+ when :starred_repo then 'rspec-expectations' # make it different from :repo
49
+ when :unstarred_repo then 'rspec-core' # make it different from :repo
50
+ when :starred_gist_id then 'e202e2fb143c54e5139a'
51
+ when :unstarred_gist_id then '8f2ef7e69ab79084d833'
52
+ when :someone_elses_gist_id then '4685e0bebbf05370abd6'
53
+ when :thread_id then '17915960'
54
+ when :id then '921225'
55
+ # NOTE: The following are confusing: they are used for filters, not value
56
+ # For instance, it's not that we must have an object with the following
57
+ # updated_at, we just use it for the since filter
58
+ when :updated_at then '2013-10-31T09:53:00Z' # TODO use helpers
59
+ # NOTE: Here's the confusion: :unread is used for the :all filter, but
60
+ # it has the reverse meaning: ?all=false will only show unread='true'
61
+ when :unread then 'false' # TODO use helpers
62
+ # NOTE: Here's more confusion: :reason is a string, but this is used for
63
+ # the boolean parameter participating:
64
+ # ?participating=true (default), only show reason = 'mention' or 'author'
65
+ # ?participating=false, show all reasons
66
+ when :reason then 'true' # TODO use helpers
67
+ end
68
+ -> { value }
69
+ end
70
+
71
+ def missing_value_for(field)
72
+ value = case field
73
+ when :user, :assignee then 'not-a-valid-user'
74
+ when :gist_id then 'not-a-valid-gist-id'
75
+ when :id then 'not-a-valid-id'
76
+ when :repo then 'not-a-valid-repo'
77
+ end
78
+ -> { value }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,14 @@
1
+ module RSpecApi
2
+ module DSL
3
+ module HttpClient
4
+ def send_request(verb, route, body, authorization)
5
+ # To be overriden
6
+ end
7
+
8
+ def last_response
9
+ # To be overriden. status MUST be a number, headers MUST be a hash
10
+ OpenStruct.new status: nil, headers: {} # body: nil,
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path("../../../../spec/dummy/config/environment.rb", __FILE__)
2
+ require 'rspec/rails'
3
+ require 'rack/test'
4
+
5
+ def app
6
+ Rails.application
7
+ end
8
+
9
+ module RSpecApi
10
+ module DSL
11
+ module HttpClient
12
+ include Rack::Test::Methods
13
+
14
+ def ciao
15
+
16
+ end
17
+
18
+ def send_request(verb, route, body, authorization)
19
+ header 'Accept', 'application/json'
20
+ send verb, route, body
21
+ end
22
+
23
+ def response
24
+ last_response
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,9 +1,18 @@
1
- module Http
2
- module Remote
3
- module Route
4
- extend ActiveSupport::Concern
1
+ require 'faraday'
2
+ require 'faraday_middleware' # TODO: use autoload, we only need EncodeJson
3
+ require 'faraday-http-cache'
4
+ require 'logger'
5
5
 
6
- def send_request(verb, route, body)
6
+ module Authorize
7
+ def authorize_with(options = {})
8
+ @authorization = options
9
+ end
10
+ end
11
+
12
+ module RSpecApi
13
+ module DSL
14
+ module HttpClient
15
+ def send_request(verb, route, body, authorization)
7
16
  logger = Logger.new 'log/faraday.log'
8
17
 
9
18
  conn = Faraday.new 'https://api.github.com/' do |c| # TODO: Pass host as a parameter
@@ -23,9 +32,8 @@ module Http
23
32
  end
24
33
  end
25
34
 
26
- def authorization
27
- # TODO: Any other way to access metadata in a before(:all) ?
28
- self.class.metadata[:rspec_api][:authorization]
35
+ def last_response
36
+ @last_response
29
37
  end
30
38
  end
31
39
  end
@@ -1,3 +1,3 @@
1
1
  module RspecApi
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-03 00:00:00.000000000 Z
11
+ date: 2013-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -28,16 +28,30 @@ dependencies:
28
28
  name: rspec-api-matchers
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ~>
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 0.5.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ~>
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 0.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-api-expectations
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rack
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -257,23 +271,20 @@ executables: []
257
271
  extensions: []
258
272
  extra_rdoc_files: []
259
273
  files:
260
- - lib/rspec-api/dsl/request/body.rb
261
- - lib/rspec-api/dsl/request/headers.rb
262
- - lib/rspec-api/dsl/request/request.rb
263
- - lib/rspec-api/dsl/request/response.rb
264
- - lib/rspec-api/dsl/request/status.rb
265
- - lib/rspec-api/dsl/request.rb
266
- - lib/rspec-api/dsl/resource.rb
267
- - lib/rspec-api/dsl/route.rb
274
+ - lib/rspec-api/dsl/accepts.rb
275
+ - lib/rspec-api/dsl/actions.rb
276
+ - lib/rspec-api/dsl/attributes.rb
277
+ - lib/rspec-api/dsl/requests.rb
278
+ - lib/rspec-api/dsl/responses.rb
268
279
  - lib/rspec-api/dsl.rb
269
- - lib/rspec-api/http/local/request.rb
270
- - lib/rspec-api/http/local/route.rb
271
- - lib/rspec-api/http/local.rb
272
- - lib/rspec-api/http/remote/request.rb
273
- - lib/rspec-api/http/remote/resource.rb
274
- - lib/rspec-api/http/remote/route.rb
275
- - lib/rspec-api/http/remote.rb
280
+ - lib/rspec-api/fixtures/empty.rb
281
+ - lib/rspec-api/fixtures/local.rb
282
+ - lib/rspec-api/fixtures/remote.rb
283
+ - lib/rspec-api/http_clients/empty.rb
284
+ - lib/rspec-api/http_clients/local.rb
285
+ - lib/rspec-api/http_clients/remote.rb
276
286
  - lib/rspec-api/version.rb
287
+ - lib/rspec-api.rb
277
288
  - MIT-LICENSE
278
289
  - README.md
279
290
  homepage: https://github.com/rspec-api/rspec-api
@@ -1,5 +0,0 @@
1
- require 'rspec-api/dsl/request/request'
2
- require 'rspec-api/dsl/request/response'
3
- require 'rspec-api/dsl/request/status'
4
- require 'rspec-api/dsl/request/headers'
5
- require 'rspec-api/dsl/request/body'
@@ -1,85 +0,0 @@
1
- require 'active_support'
2
- require 'rspec-api/matchers'
3
-
4
- module DSL
5
- module Request
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- # Creates an example group for expectations on the response body of the
10
- # last API request and runs it to verify that it matches best practices:
11
- # * if response is succesful and has a body
12
- # - the body should be a JSON-marshalled Array or Hash
13
- # - if request has a callback parameter, the body should be JSONP
14
- # - if request has a sort parameter, the body should be sorted
15
- # - if request has a filter parameter, the body should be filtered
16
- # - if custom expectations are passed, they should pass
17
- # - if some attributes are expected, the body should include them
18
- def should_respond_with_expected_body(options = {})
19
- context 'responds with a body that' do
20
- it { should_be_valid_json options[:type] }
21
- it { should_be_wrapped_by options[:callbacks] }
22
- it { should_be_sorted_by options[:sorts] }
23
- it { should_be_filtered_by options[:filters] }
24
- it { should_have_attributes options[:attributes] }
25
- end
26
- end
27
- end
28
-
29
- def should_be_valid_json(type)
30
- expect(response).to be_valid_json_if response_is_successful?, type
31
- end
32
-
33
- # If the request had a 'callback' query parameter, then the body should be
34
- # JSONP, otherwise it should not. For instance if the request was
35
- # `GET /?method=alert` and the request `accepts_callback :method`, then
36
- # the body must be a JSON wrapped in the alert(...) callback
37
- # The +callback+ param says how the request might have been made, e.g.
38
- # name: 'method', value: 'alert'... however to make sure that it was
39
- # really made, we need to check that request_params['method'] is present
40
- # and that is actually 'alert'
41
- def should_be_wrapped_by(callback_params_sets)
42
- callback_params = response_is_successful? && get_request_param_for_list(callback_params_sets)
43
- value = callback_params[:value] if callback_params
44
- expect(response).to be_a_jsonp_if callback_params, value
45
- end
46
-
47
- def should_be_sorted_by(sort_params_sets)
48
- sort_params = response_is_successful? && get_request_param_for_list(sort_params_sets)
49
- options = sort_params ? sort_params.slice(:by, :verse) : {}
50
- expect(response).to be_sorted_if sort_params, options
51
- end
52
-
53
- def should_be_filtered_by(filter_params_sets)
54
- # TODO: The following is just so the condition does not match if it's nil
55
- # but this should be fixed in get_request_param_for_list
56
- if filter_params_sets
57
- filter_params_sets = filter_params_sets.dup
58
- filter_params_sets.each{|x| x[:value] = request_params.fetch(x[:name].to_s, :something_nil)}
59
- end
60
- filter_params = response_is_successful? && get_request_param_for_list(filter_params_sets)
61
- value = filter_params[:value] if filter_params
62
- options = filter_params ? filter_params.slice(:by, :comparing_with) : {}
63
- expect(response).to be_filtered_if filter_params, value, options
64
- end
65
-
66
- def should_have_attributes(attributes)
67
- expect(response).to have_attributes_if response_is_successful?, attributes
68
- end
69
-
70
- def get_request_param_for_list(params_sets)
71
- (params_sets || []).find do |params|
72
- conditions = []
73
- conditions << (request_params[params[:name].to_s] == params[:value])
74
- params.fetch(:extra_fields, {}).each do |name, value|
75
- conditions << (request_params[name.to_s] == value)
76
- end
77
- conditions.all?
78
- end
79
- end
80
-
81
- def response_is_successful?
82
- response.status < 400 && !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(response.status)
83
- end
84
- end
85
- end
@@ -1,40 +0,0 @@
1
- require 'active_support'
2
- require 'rack/utils'
3
- require 'rspec-api/matchers'
4
-
5
- module DSL
6
- module Request
7
- extend ActiveSupport::Concern
8
-
9
- module ClassMethods
10
- # Creates an example group for expectations on the response headers of
11
- # last API request and runs it to verify that it matches best practices:
12
- # * if request has entity body, the Content-Type header should be JSON
13
- # * if request has pages, the Link header should have a 'rel=prev' link
14
- def should_respond_with_expected_headers(options = {})
15
- context 'responds with headers that' do
16
- it { should_include_content_type :json }
17
- it { should_have_prev_page_link options[:page] }
18
- end
19
- end
20
- end
21
-
22
- private
23
-
24
- def should_include_content_type(type)
25
- expect(response).to include_content_type_if response_has_body?, type
26
- end
27
-
28
- def should_have_prev_page_link(page)
29
- expect(response).to have_prev_page_link_if request_is_paginated?(page)
30
- end
31
-
32
- def response_has_body?
33
- !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(response.status)
34
- end
35
-
36
- def request_is_paginated?(page_parameter)
37
- request_params.key? page_parameter.to_s
38
- end
39
- end
40
- end
@@ -1,14 +0,0 @@
1
- require 'active_support'
2
-
3
- module DSL
4
- module Request
5
- extend ::ActiveSupport::Concern
6
-
7
- module ClassMethods
8
- end
9
-
10
- def request_params
11
- {} # To be overriden by more specific modules
12
- end
13
- end
14
- end
@@ -1,86 +0,0 @@
1
- require 'active_support'
2
-
3
- module DSL
4
- module Request
5
- extend ::ActiveSupport::Concern
6
-
7
- module ClassMethods
8
- # Runs a set of expectations on the result of the last API request.
9
- # The most basic way to call `respond_with` is with a status code:
10
- #
11
- # respond_with :ok
12
- #
13
- # This simple line will run a number of expectations, based on best
14
- # practices that any RESTful API is expected to match:
15
- # * the returned HTTP status will be matched against 200 OK
16
- # * the response headers will be checked for Content-Type etc.
17
- # * the respone body will be checked for attributes etc.
18
- #
19
- # Additionally, the user can specify custom expectations for the
20
- # response by passing a block to the method:
21
- #
22
- # respond_with :ok do |response|
23
- # expect(response).to be_successful?
24
- # end
25
- #
26
- # In this case, after all the *implicit* expectations are run, the
27
- # JSON-parsed response body is passed to the block to make sure that
28
- # (in the example above), the body is a hash with a 'color' key and
29
- # the 'green' value
30
- def respond_with(status_symbol, &block)
31
- should_respond_with_status status_symbol
32
- should_respond_with_expected_headers headers_options
33
- should_respond_with_expected_body body_options
34
- should_match_custom_response_expectations &block if block_given?
35
- end
36
-
37
- private
38
-
39
- def headers_options
40
- {page: rspec_api.fetch(:page, {})[:name].to_s}
41
- end
42
-
43
- def body_options
44
- {
45
- type: rspec_api[:array] ? Array : Hash,
46
- callbacks: rspec_api.fetch(:callbacks, []),
47
- sorts: rspec_api.fetch(:sorts, []),
48
- filters: rspec_api.fetch(:filters, []),
49
- attributes: rspec_api.fetch(:attributes, {})
50
- }
51
- end
52
-
53
- def should_match_custom_response_expectations(&block)
54
- it { instance_exec response, @url_params, &block }
55
- end
56
- end
57
-
58
- def response
59
- # To be overriden by more specific modules
60
- OpenStruct.new # body: nil, status: nil, headers: {}
61
- end
62
-
63
- def response_body
64
- JSON response_body_without_callbacks
65
- rescue JSON::ParserError, JSON::GeneratorError
66
- nil
67
- end
68
-
69
- def response_headers
70
- response.headers || {}
71
- end
72
-
73
- def response_status
74
- response.status
75
- end
76
-
77
- private
78
-
79
- def response_body_without_callbacks
80
- body = response.body
81
- # TODO: extract the 'a_callback' constant
82
- callback_pattern = %r[a_callback\((.*?)\)]
83
- body =~ callback_pattern ? body.match(callback_pattern)[1] : body
84
- end
85
- end
86
- end
@@ -1,18 +0,0 @@
1
- require 'active_support'
2
- require 'rspec-api/matchers'
3
-
4
- module DSL
5
- module Request
6
- extend ::ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- # Creates an example group for expectations on the HTTP status code of the
10
- # last API request and runs it to verify that it matches +status+.
11
- def should_respond_with_status(status)
12
- context 'responds with a status code that' do
13
- it { expect(response).to have_status status }
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,68 +0,0 @@
1
- require 'active_support'
2
- module DSL
3
- module Resource
4
- extend ::ActiveSupport::Concern
5
-
6
- module ClassMethods
7
- def rspec_api
8
- metadata[:rspec_api]
9
- end
10
-
11
- def self.define_action(verb)
12
- define_method verb do |route, args = {}, &block|
13
- rspec_api.merge! array: args.delete(:array), verb: verb, route: route
14
- args.merge! rspec_api_dsl: :route
15
- describe("#{verb.upcase} #{route}", args, &block)
16
- end
17
- end
18
-
19
- define_action :get
20
- define_action :put
21
- define_action :patch
22
- define_action :post
23
- define_action :delete
24
-
25
-
26
- def has_attribute(name, options = {}, &block)
27
- if block_given?
28
- # options[:type] can be symbol, hash or array
29
- # but if you have a block we must make it a hash
30
- options[:type] = Hash[*Array.wrap(options[:type]).map{|x| x.is_a?(Hash) ? [x.keys.first, x.values.first] : [x, {}]}.flatten] unless options[:type].is_a? Hash
31
- # we only set the block as the new format of Object and Array
32
- nest_attributes(options[:type], &Proc.new)
33
- end
34
- if @attribute_ancestors.present?
35
- hash = @attribute_ancestors.last
36
- hash.slice(:object, :array).each do |type, _|
37
- (hash[type] ||= {})[name] = options
38
- end
39
- else
40
- hash = (rspec_api[:attributes] ||= {})
41
- hash[name] = options
42
- end
43
- end
44
-
45
- def nest_attributes(hash, &block)
46
- (@attribute_ancestors ||= []).push hash
47
- yield
48
- @attribute_ancestors.pop
49
- end
50
-
51
- def accepts_page(page_parameter)
52
- rspec_api[:page] = {name: page_parameter, value: 2}
53
- end
54
-
55
- def accepts_sort(sort_parameter, options={})
56
- (rspec_api[:sorts] ||= []) << options.merge(name: 'sort', value: sort_parameter)
57
- end
58
-
59
- def accepts_filter(filter_parameter, options={})
60
- (rspec_api[:filters] ||= []) << options.merge(name: filter_parameter)
61
- end
62
-
63
- def accepts_callback(callback_parameter)
64
- (rspec_api[:callbacks] ||= []) << {name: callback_parameter.to_s, value: 'a_callback'}
65
- end
66
- end
67
- end
68
- end
@@ -1,135 +0,0 @@
1
- require 'active_support/core_ext/object' # present?
2
-
3
-
4
- module DSL
5
- module Route
6
- extend ActiveSupport::Concern
7
-
8
- def send_request(verb, route, body)
9
- # To be overriden by more specific modules
10
- end
11
-
12
- module ClassMethods
13
- def request(*args, &block)
14
- text, values = parse_request_arguments args
15
- sets_of_parameters.each do |params|
16
- request_with_params text, values.merge(params), &block
17
- end
18
- end
19
-
20
- def setup_fixtures
21
- # To be overriden by more specific modules
22
- end
23
-
24
- def existing(field)
25
- # To be overriden by more specific modules
26
- end
27
-
28
- private
29
-
30
- def request_with_params(text, values = {}, &block)
31
- context request_description(text, values), rspec_api_dsl: :request do
32
- # NOTE: Having setup_fixtures inside the context sets up different
33
- # fixtures for each `request` inside the same `get`. This might be
34
- # a little slower on the DB, but ensures that two `request`s do not
35
- # conflict. For instance, if you have two `request` inside a `delete`
36
- # and the first deletes an instance, the second `request` is no
37
- # affected.
38
- setup_fixtures
39
- setup_request rspec_api[:verb], rspec_api[:route], values
40
- instance_eval(&block) if block_given?
41
- end
42
- end
43
-
44
- def sets_of_parameters
45
- [].tap do |sets_of_params|
46
- sets_of_params.push no_params
47
- if rspec_api[:callbacks]
48
- rspec_api[:callbacks].each do |callback|
49
- sets_of_params.push callback_params(callback)
50
- end
51
- end
52
- if rspec_api[:array]
53
- if rspec_api[:sorts]
54
- rspec_api[:sorts].each do |sort|
55
- sets_of_params.push sort_params(sort)
56
- end
57
- end
58
- if rspec_api[:filters]
59
- rspec_api[:filters].each do |filter|
60
- sets_of_params.push filter_params(filter)
61
- end
62
- end
63
- if rspec_api[:page]
64
- sets_of_params.push page_params
65
- end
66
- end
67
- end
68
- end
69
-
70
- def no_params
71
- {} # always send the original request without extra parameters
72
- end
73
-
74
- def sort_params(sort)
75
- {}.tap do |params|
76
- params[sort[:name]] = sort[:value]
77
- sort.fetch(:extra_fields, {}).each do |name, value|
78
- params[name] = value
79
- end
80
- end
81
- end
82
-
83
- def page_params
84
- {}.tap do |params|
85
- params[rspec_api[:page][:name]] = rspec_api[:page][:value]
86
- end
87
- end
88
-
89
- def filter_params(filter)
90
- {}.tap do |params|
91
- params[filter[:name]] = existing filter[:by]
92
- end
93
- end
94
-
95
- def callback_params(callback)
96
- {}.tap do |params|
97
- params[callback[:name]] = callback[:value]
98
- end
99
- end
100
-
101
- def setup_request(verb, route, values)
102
- request = Proc.new {
103
- interpolated_route, body = route.dup, values.dup
104
- body.keys.each do |key|
105
- if interpolated_route[":#{key}"]
106
- value = body.delete(key)
107
- value = value.call if value.is_a?(Proc)
108
- interpolated_route[":#{key}"] = value.to_s
109
- (@url_params ||= {})[key] = value
110
- else
111
- body[key] = body[key].call if body[key].is_a?(Proc)
112
- end
113
- end
114
- [interpolated_route, body]
115
- }
116
- before(:all) { send_request verb, *instance_eval(&request) }
117
- end
118
-
119
- def request_description(text, values)
120
- if values.empty?
121
- 'by default'
122
- else
123
- text = "with" unless text.present?
124
- "#{text} #{values.map{|k,v| "#{k}#{" #{v}" unless v.is_a?(Proc)}"}.to_sentence}"
125
- end
126
- end
127
-
128
- def parse_request_arguments(args)
129
- text = args.first.is_a?(String) ? args[0] : ''
130
- values = args.first.is_a?(String) ? args[1] : args[0]
131
- [text, values || {}] # NOTE: In Ruby 2.0 we could write values.to_h
132
- end
133
- end
134
- end
135
- end
@@ -1,11 +0,0 @@
1
- require 'rack/test'
2
- require 'rspec-api/http/local/route'
3
- require 'rspec-api/http/local/request'
4
-
5
- def app
6
- Rails.application
7
- end
8
-
9
- RSpec.configuration.include Rack::Test::Methods, rspec_api_dsl: :route
10
- RSpec.configuration.include Http::Local::Route, rspec_api_dsl: :route
11
- RSpec.configuration.include Http::Local::Request, rspec_api_dsl: :request
@@ -1,15 +0,0 @@
1
- module Http
2
- module Local
3
- module Request
4
- extend ActiveSupport::Concern
5
-
6
- def response
7
- last_response
8
- end
9
-
10
- def request_params
11
- last_request.params
12
- end
13
- end
14
- end
15
- end
@@ -1,12 +0,0 @@
1
- module Http
2
- module Local
3
- module Route
4
- extend ActiveSupport::Concern
5
-
6
- def send_request(verb, route, body)
7
- header 'Accept', 'application/json'
8
- send verb, route, body
9
- end
10
- end
11
- end
12
- end
@@ -1,23 +0,0 @@
1
- require 'faraday'
2
- require 'faraday_middleware' # TODO: use autoload, we only need EncodeJson
3
- require 'faraday-http-cache'
4
-
5
- # faraday-http-cache is a great gem that correctly ignores Private Cache
6
- # For the sake of saving Github calls, let's cache Private as well!
7
- module Faraday
8
- class HttpCache
9
- class CacheControl
10
- def private?
11
- false
12
- end
13
- end
14
- end
15
- end
16
-
17
- require 'rspec-api/http/remote/resource'
18
- require 'rspec-api/http/remote/route'
19
- require 'rspec-api/http/remote/request'
20
-
21
- RSpec.configuration.include Http::Remote::Resource, rspec_api_dsl: :resource
22
- RSpec.configuration.include Http::Remote::Request, rspec_api_dsl: :request
23
- RSpec.configuration.include Http::Remote::Route, rspec_api_dsl: :route
@@ -1,15 +0,0 @@
1
- module Http
2
- module Remote
3
- module Request
4
- extend ActiveSupport::Concern
5
-
6
- def response
7
- @last_response
8
- end
9
-
10
- def request_params
11
- @last_request.params
12
- end
13
- end
14
- end
15
- end
@@ -1,13 +0,0 @@
1
- module Http
2
- module Remote
3
- module Resource
4
- extend ActiveSupport::Concern
5
-
6
- module ClassMethods
7
- def authorize_with(options = {})
8
- rspec_api[:authorization] = options
9
- end
10
- end
11
- end
12
- end
13
- end