rspec-api 0.4.0 → 0.5.0

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 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