shokkenki-consumer 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/lib/shokkenki/consumer/configuration/provider_configuration.rb +30 -0
  2. data/lib/shokkenki/consumer/configuration/session.rb +38 -0
  3. data/lib/shokkenki/consumer/consumer.rb +9 -0
  4. data/lib/shokkenki/consumer/dsl/http_methods.rb +16 -0
  5. data/lib/shokkenki/consumer/dsl/order.rb +70 -0
  6. data/lib/shokkenki/consumer/dsl/session.rb +13 -0
  7. data/lib/shokkenki/consumer/dsl/term_dsl.rb +13 -0
  8. data/lib/shokkenki/consumer/model/fixture.rb +21 -0
  9. data/lib/shokkenki/consumer/model/interaction.rb +29 -0
  10. data/lib/shokkenki/consumer/model/patronage.rb +31 -0
  11. data/lib/shokkenki/consumer/model/provider.rb +33 -0
  12. data/lib/shokkenki/consumer/model/role.rb +27 -0
  13. data/lib/shokkenki/consumer/model/simplification.rb +16 -0
  14. data/lib/shokkenki/consumer/model/ticket.rb +38 -0
  15. data/lib/shokkenki/consumer/rspec/example_group_binding.rb +17 -0
  16. data/lib/shokkenki/consumer/rspec/hooks.rb +32 -0
  17. data/lib/shokkenki/consumer/rspec/rspec_configuration.rb +25 -0
  18. data/lib/shokkenki/consumer/rspec.rb +2 -0
  19. data/lib/shokkenki/consumer/session.rb +83 -0
  20. data/lib/shokkenki/consumer/stubber/http_stubber.rb +97 -0
  21. data/lib/shokkenki/consumer/stubber/interaction.rb +51 -0
  22. data/lib/shokkenki/consumer/stubber/interactions.rb +41 -0
  23. data/lib/shokkenki/consumer/stubber/interactions_middleware.rb +34 -0
  24. data/lib/shokkenki/consumer/stubber/rack_response.rb +28 -0
  25. data/lib/shokkenki/consumer/stubber/request.rb +72 -0
  26. data/lib/shokkenki/consumer/stubber/restful_middleware.rb +33 -0
  27. data/lib/shokkenki/consumer/stubber/server.rb +69 -0
  28. data/lib/shokkenki/consumer/stubber/server_application_error.rb +16 -0
  29. data/lib/shokkenki/consumer/stubber/stub_server_middleware.rb +46 -0
  30. data/lib/shokkenki/consumer/stubber/stubbed_response_middleware.rb +34 -0
  31. data/lib/shokkenki/consumer/stubber/unmatched_requests_middleware.rb +22 -0
  32. data/lib/shokkenki/consumer/stubber/unused_interactions_middleware.rb +21 -0
  33. data/lib/shokkenki/consumer/version.rb +7 -0
  34. metadata +258 -0
@@ -0,0 +1,51 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'shokkenki/term/term_factory'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Stubber
7
+ class Interaction
8
+
9
+ attr_reader :label, :request, :response, :time, :matched_requests
10
+
11
+ def self.from_json json
12
+ attributes = json.with_indifferent_access
13
+
14
+ new(
15
+ :label => attributes[:label],
16
+ :request => Shokkenki::Term::TermFactory.from_json(attributes[:request]),
17
+ :response => Shokkenki::Term::TermFactory.from_json(attributes[:response]),
18
+ :time => attributes[:time]
19
+ )
20
+ end
21
+
22
+ def initialize attributes
23
+ @label = attributes[:label]
24
+ @request = attributes[:request]
25
+ @response = attributes[:response]
26
+ @time = attributes[:time]
27
+ @matched_requests = []
28
+ end
29
+
30
+ def to_hash
31
+ {
32
+ :label => @label
33
+ }
34
+ end
35
+
36
+ def generate_response
37
+ @response.example
38
+ end
39
+
40
+ def match_request? request
41
+ @request.match? request
42
+ end
43
+
44
+ def add_match request
45
+ @matched_requests << request
46
+ request.interaction = self
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Stubber
4
+ class Interactions
5
+
6
+ attr_reader :interactions, :requests
7
+
8
+ def initialize
9
+ @interactions = []
10
+ @requests = []
11
+ end
12
+
13
+ def find request
14
+ matching_interaction = @interactions.find do |interaction|
15
+ interaction.match_request? request
16
+ end
17
+ matching_interaction.add_match(request) if matching_interaction
18
+ @requests << request
19
+ matching_interaction
20
+ end
21
+
22
+ def delete_all
23
+ @interactions.clear
24
+ @requests.clear
25
+ end
26
+
27
+ def add interaction
28
+ @interactions << interaction
29
+ end
30
+
31
+ def unmatched_requests
32
+ @requests.select{ |r| r.interaction.nil? }
33
+ end
34
+
35
+ def unused_interactions
36
+ @interactions.select { |i| i.matched_requests.empty? }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'interaction'
2
+ require_relative 'restful_middleware'
3
+ require 'json'
4
+
5
+ module Shokkenki
6
+ module Consumer
7
+ module Stubber
8
+ class InteractionsMiddleware < RestfulMiddleware
9
+
10
+ def initialize interactions
11
+ @interactions = interactions
12
+ end
13
+
14
+ get { |env| [200, {}, ["[]"]] }
15
+
16
+ post do |env|
17
+ @interactions.add Interaction.from_json(body_json(env['rack.input']))
18
+ [204, {}, []]
19
+ end
20
+
21
+ delete do |env|
22
+ @interactions.delete_all
23
+ [204, {}, []]
24
+ end
25
+
26
+ private
27
+
28
+ def body_json body_io
29
+ JSON.parse body_io.read
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module Stubber
6
+ class RackResponse
7
+
8
+ def self.from_interaction interaction
9
+ defaults = { :status => 200 }
10
+ response = defaults.merge interaction.generate_response
11
+ [response[:status], as_rack_headers(response[:headers]), [response[:body]]]
12
+ end
13
+
14
+ def self.as_rack_headers headers
15
+ (headers || []).inject({}) do |h, key_value|
16
+ k, v = key_value
17
+ h[as_header_name(k)] = v.to_s
18
+ h
19
+ end
20
+ end
21
+
22
+ def self.as_header_name name
23
+ name.to_s.split('-').map{ |word| word.titleize }.join('-')
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ require 'uri'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module Stubber
6
+ class Request
7
+
8
+ attr_reader :path, :method, :body, :query, :headers
9
+ attr_accessor :interaction
10
+
11
+ def self.from_rack rack_env
12
+ env = rack_env.dup
13
+ new(
14
+ :path => env['PATH_INFO'],
15
+ :method => env['REQUEST_METHOD'].downcase,
16
+ :body => env['rack.input'].read,
17
+ :query => query_from(env['QUERY_STRING']),
18
+ :headers => headers_from(env)
19
+ )
20
+ end
21
+
22
+ def initialize attributes
23
+ @path = attributes[:path]
24
+ @method = attributes[:method]
25
+ @body = attributes[:body]
26
+ @query = attributes[:query]
27
+ @headers = attributes[:headers]
28
+ end
29
+
30
+ def to_hash
31
+ hash = {
32
+ :path => @path,
33
+ :method => @method,
34
+ :headers => @headers,
35
+ :query => @query,
36
+ :body => @body
37
+ }
38
+ hash.merge!(:interaction => @interaction.to_hash) if @interaction
39
+ hash
40
+ end
41
+
42
+ def self.headers_from env
43
+ env.reject{ |k, v|
44
+ [
45
+ 'PATH_INFO',
46
+ 'REQUEST_METHOD',
47
+ 'QUERY_STRING'
48
+ ].include?(k)
49
+ }.
50
+ reject { |k, v| k.start_with?('rack.') }.
51
+ reject { |k, v| k.start_with?('async.') }.
52
+ inject({}) do |headers, key_value|
53
+ headers[as_header_name(key_value[0])] = key_value[1]
54
+ headers
55
+ end
56
+ end
57
+
58
+ def self.query_from query_string
59
+ query_string.split('&').inject({}) do |query, param|
60
+ k, v = param.split '='
61
+ query[k.to_sym] = URI.decode v
62
+ query
63
+ end
64
+ end
65
+
66
+ def self.as_header_name name
67
+ name.to_s.gsub(/^HTTP_/, '').gsub('_', '-').downcase.to_sym
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,33 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Stubber
4
+ class RestfulMiddleware
5
+
6
+ def self.post &block
7
+ handlers[:post] = block
8
+ end
9
+
10
+ def self.delete &block
11
+ handlers[:delete] = block
12
+ end
13
+
14
+ def self.get &block
15
+ handlers[:get] = block
16
+ end
17
+
18
+ def self.handlers
19
+ @handlers ||= {}
20
+ end
21
+
22
+ def call env
23
+ handler = self.class.handlers[env['REQUEST_METHOD'].downcase.to_sym]
24
+ handler ? instance_exec(env, &handler) : [405, {'Allow' => allowed_methods.join(', ')}, []]
25
+ end
26
+
27
+ def allowed_methods
28
+ self.class.handlers.keys.map {|k| k.to_s.upcase }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,69 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'rack'
4
+ require 'rack/handler/webrick'
5
+ require_relative 'server_application_error'
6
+
7
+ # pinched from https://github.com/jnicklas/capybara/blob/master/lib/capybara/server.rb
8
+ module Shokkenki
9
+ module Consumer
10
+ module Stubber
11
+ class Server
12
+
13
+ attr_reader :app, :port, :host, :server_thread, :error
14
+
15
+ def initialize attributes
16
+ @app = attributes[:app]
17
+ @server_thread = nil # suppress warnings
18
+ @host = attributes[:host]
19
+ @port = attributes[:port]
20
+ end
21
+
22
+ def error
23
+ @app.error
24
+ end
25
+
26
+ def assert_ok!
27
+ raise ServerApplicationError.new(error) if error
28
+ end
29
+
30
+ def responsive?
31
+ return false if server_thread && server_thread.join(0)
32
+ res = Net::HTTP.start(host, @port) do |http|
33
+ http.get @app.identify_path
34
+ end
35
+ assert_ok!
36
+ ok?(res) && is_app?(res)
37
+ rescue SystemCallError
38
+ return false
39
+ end
40
+
41
+ def is_app? response
42
+ response.body.to_s == @app.object_id.to_s
43
+ end
44
+
45
+ def ok? response
46
+ response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
47
+ end
48
+
49
+ def run app, host, port
50
+ Rack::Handler::WEBrick.run(app, :Host => host, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
51
+ end
52
+
53
+ def start
54
+ @server_thread = Thread.new do
55
+ run @app, @host, @port
56
+ end
57
+
58
+ Timeout.timeout(10) { @server_thread.join(0.1) until responsive? }
59
+ rescue Timeout::Error
60
+ raise "Rack application #{@app} timed out during boot"
61
+ end
62
+
63
+ def shutdown
64
+ server_thread.kill
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Stubber
4
+ class ServerApplicationError < StandardError
5
+ def initialize error
6
+ super "An error occurred in the stub server: #{error.message}"
7
+ @error = error
8
+ end
9
+
10
+ def set_backtrace backtrace
11
+ super (@error.backtrace || []) + backtrace
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'interactions'
2
+ require_relative 'interactions_middleware'
3
+ require_relative 'stubbed_response_middleware'
4
+ require_relative 'unmatched_requests_middleware'
5
+ require_relative 'unused_interactions_middleware'
6
+
7
+ # adapted from middleware in https://github.com/jnicklas/capybara/blob/master/lib/capybara/server.rb
8
+ module Shokkenki
9
+ module Consumer
10
+ module Stubber
11
+ class StubServerMiddleware
12
+
13
+ attr_accessor :error
14
+
15
+ def initialize
16
+ @interactions = Interactions.new
17
+ @middlewares = {
18
+ %r{^#{identify_path}} => lambda(&method(:identify)),
19
+ %r{^/shokkenki/interactions/unused} => UnusedInteractionsMiddleware.new(@interactions),
20
+ %r{^/shokkenki/interactions} => InteractionsMiddleware.new(@interactions),
21
+ %r{^/shokkenki/requests/unmatched} => UnmatchedRequestsMiddleware.new(@interactions),
22
+ /.*/ => StubbedResponseMiddleware.new(@interactions)
23
+ }
24
+ end
25
+
26
+ def call env
27
+ begin
28
+ handler = @middlewares.find {|path, m| path.match(env['PATH_INFO']) }[1]
29
+ handler.call env
30
+ rescue StandardError => e
31
+ @error = e unless @error
32
+ raise e
33
+ end
34
+ end
35
+
36
+ def identify env
37
+ [200, {}, [object_id.to_s]]
38
+ end
39
+
40
+ def identify_path
41
+ '/shokkenki/__identify__'
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+ require_relative 'request'
3
+ require_relative 'rack_response'
4
+
5
+ module Shokkenki
6
+ module Consumer
7
+ module Stubber
8
+ class StubbedResponseMiddleware
9
+
10
+ def initialize interactions
11
+ @interactions = interactions
12
+ end
13
+
14
+ def call env
15
+ request = Request.from_rack env
16
+ interaction = @interactions.find request
17
+ interaction ? RackResponse.from_interaction(interaction) : no_interaction(request)
18
+ end
19
+
20
+ private
21
+
22
+ def no_interaction request
23
+ body = {
24
+ :shokkenki => {
25
+ :message => 'No matching responses were found for the request.',
26
+ :request => request.to_hash
27
+ }
28
+ }
29
+ [404, {'Content-Type' => 'application/json'}, [body.to_json]]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'interaction'
2
+ require_relative 'restful_middleware'
3
+ require 'json'
4
+
5
+ module Shokkenki
6
+ module Consumer
7
+ module Stubber
8
+ class UnmatchedRequestsMiddleware < RestfulMiddleware
9
+
10
+ def initialize interactions
11
+ @interactions = interactions
12
+ end
13
+
14
+ get do |env|
15
+ requests = @interactions.unmatched_requests
16
+ json = JSON.pretty_generate(requests.collect{|r| r.to_hash})
17
+ [200, {'Content-Type' => 'application/json'}, [json]]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'restful_middleware'
2
+ require 'json'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Stubber
7
+ class UnusedInteractionsMiddleware < RestfulMiddleware
8
+
9
+ def initialize interactions
10
+ @interactions = interactions
11
+ end
12
+
13
+ get do |env|
14
+ interactions = @interactions.unused_interactions
15
+ json = JSON.pretty_generate(interactions.collect{|r| r.to_hash})
16
+ [200, {'Content-Type' => 'application/json'}, [json]]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Version
4
+ STRING = '0.0.2'
5
+ end
6
+ end
7
+ end