mirage 2.4.2 → 3.0.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. data/.simplecov +6 -0
  2. data/Gemfile +11 -3
  3. data/Gemfile.lock +41 -14
  4. data/VERSION +1 -1
  5. data/features/client/clear.feature +41 -50
  6. data/features/client/configure.feature +2 -2
  7. data/features/client/put.feature +17 -6
  8. data/features/client/requests.feature +5 -9
  9. data/features/client/start.feature +19 -11
  10. data/features/client/stop.feature +10 -44
  11. data/features/server/commandline_interface/start.feature +2 -14
  12. data/features/server/commandline_interface/stop.feature +6 -4
  13. data/features/server/logging.feature +2 -2
  14. data/features/server/prime.feature +11 -66
  15. data/features/server/requests/delete.feature +34 -33
  16. data/features/server/requests/get.feature +21 -18
  17. data/features/server/save_and_revert.feature +24 -11
  18. data/features/server/templates/delete.feature +29 -32
  19. data/features/server/templates/get.feature +44 -25
  20. data/features/server/templates/put/put.feature +55 -78
  21. data/features/server/templates/put/put_with_substitutions.feature +12 -32
  22. data/features/server/templates/put/required_content.feature +118 -0
  23. data/features/step_definitions/my_steps.rb +51 -6
  24. data/features/support/env.rb +1 -1
  25. data/features/support/hooks.rb +2 -5
  26. data/{lib/mirage/client → features/support}/web.rb +14 -3
  27. data/lib/mirage/client.rb +5 -2
  28. data/lib/mirage/client/client.rb +22 -129
  29. data/lib/mirage/client/request.rb +25 -0
  30. data/lib/mirage/client/requests.rb +13 -0
  31. data/lib/mirage/client/runner.rb +4 -4
  32. data/lib/mirage/client/template.rb +108 -0
  33. data/lib/mirage/client/template_configuration.rb +22 -0
  34. data/lib/mirage/client/templates.rb +26 -0
  35. data/mirage.gemspec +42 -22
  36. data/mirage_server.rb +1 -135
  37. data/rakefile +22 -7
  38. data/server/app.rb +4 -0
  39. data/server/binary_data_checker.rb +15 -0
  40. data/server/helpers.rb +28 -0
  41. data/server/mock_response.rb +140 -58
  42. data/server/server.rb +167 -0
  43. data/spec/{cli_bridge_spec.rb → client/cli_bridge_spec.rb} +15 -11
  44. data/spec/client/client_spec.rb +139 -0
  45. data/spec/client/request_spec.rb +52 -0
  46. data/spec/client/requests_spec.rb +10 -0
  47. data/spec/{runner_spec.rb → client/runner_spec.rb} +3 -3
  48. data/spec/client/template_configuration_spec.rb +32 -0
  49. data/spec/client/template_spec.rb +241 -0
  50. data/spec/client/templates_spec.rb +79 -0
  51. data/spec/resources/binary.file +0 -0
  52. data/spec/server/binary_data_checker_spec.rb +22 -0
  53. data/spec/server/helpers_spec.rb +34 -0
  54. data/spec/server/mock_response_spec.rb +526 -0
  55. data/spec/server/server_spec.rb +132 -0
  56. data/spec/spec_helper.rb +61 -2
  57. data/test.html +12 -0
  58. data/test.rb +20 -17
  59. data/todo.lst +2 -0
  60. data/views/index.haml +22 -0
  61. data/views/response.haml +24 -0
  62. metadata +134 -49
  63. data/features/server/templates/put/put_as_default.feature +0 -42
  64. data/features/server/templates/put/put_with_delay.feature +0 -8
  65. data/features/server/templates/put/put_with_pattern.feature +0 -80
  66. data/lib/mirage/client/response.rb +0 -29
  67. data/spec/client_spec.rb +0 -38
  68. data/views/index.erb +0 -28
data/mirage_server.rb CHANGED
@@ -4,17 +4,13 @@ $LOAD_PATH.unshift("#{ROOT_DIR}/lib")
4
4
  $LOAD_PATH.unshift("#{ROOT_DIR}/server")
5
5
 
6
6
  require 'sinatra/base'
7
-
8
7
  require 'extensions/object'
9
8
  require 'extensions/hash'
10
- require 'mock_response'
11
-
9
+ require 'app'
12
10
  require 'mirage/client'
13
11
 
14
12
  module Mirage
15
-
16
13
  class Server < Sinatra::Base
17
-
18
14
  configure do
19
15
  options = Hash[*ARGV]
20
16
  set :defaults, options["defaults"]
@@ -31,136 +27,6 @@ module Mirage
31
27
  use Rack::CommonLogger, log_file
32
28
  enable :logging
33
29
  end
34
-
35
- REQUESTS= {}
36
-
37
- put '/mirage/templates/*' do |name|
38
- response = request.body.read
39
-
40
- MockResponse.new(name,
41
- response,
42
- :content_type => @env['CONTENT_TYPE'],
43
- :http_method => @env['HTTP_X_MIRAGE_METHOD'],
44
- :status => @env['HTTP_X_MIRAGE_STATUS'],
45
- :pattern => @env['HTTP_X_MIRAGE_PATTERN'],
46
- :delay => @env['HTTP_X_MIRAGE_DELAY'].to_f,
47
- :default => @env['HTTP_X_MIRAGE_DEFAULT'],
48
- :file => @env['HTTP_X_MIRAGE_FILE']).response_id.to_s
49
- end
50
-
51
- ['get', 'post', 'delete', 'put'].each do |http_method|
52
- send(http_method, '/mirage/responses/*') do |name|
53
- body, query_string = Rack::Utils.unescape(request.body.read.to_s), request.query_string
54
-
55
- begin
56
- record = MockResponse.find(body, query_string, name, http_method)
57
- rescue ServerResponseNotFound
58
- record = MockResponse.find_default(body, http_method, name, query_string)
59
- end
60
-
61
- REQUESTS[record.response_id] = body.empty? ? query_string : body
62
-
63
- send_response(record, body, request, query_string)
64
- end
65
- end
66
-
67
- delete '/mirage/templates/:id' do
68
- MockResponse.delete(response_id)
69
- REQUESTS.delete(response_id)
70
- 200
71
- end
72
-
73
- delete '/mirage/requests' do
74
- REQUESTS.clear
75
- 200
76
- end
77
-
78
- delete '/mirage/requests/:id' do
79
- REQUESTS.delete(response_id)
80
- 200
81
- end
82
-
83
- delete '/mirage/templates' do
84
- REQUESTS.clear
85
- MockResponse.delete_all
86
- 200
87
- end
88
-
89
- get '/mirage/templates/:id' do
90
- send_response(MockResponse.find_by_id(response_id))
91
- end
92
-
93
- get '/mirage/requests/:id' do
94
- REQUESTS[response_id] || 404
95
- end
96
-
97
- get '/mirage' do
98
- @responses = {}
99
-
100
- MockResponse.all.each do |response|
101
- pattern = response.pattern.is_a?(Regexp) ? "pattern = #{response.pattern.source}" : ''
102
- delay = response.delay > 0 ? "delay = #{response.delay}" : ''
103
- pattern << ' ,' unless pattern.empty? || delay.empty?
104
- @responses["#{response.name}#{'/*' if response.default?}: #{pattern} #{delay}"] = response
105
- end
106
- erb :index
107
- end
108
-
109
-
110
- put '/mirage/defaults' do
111
- MockResponse.delete_all
112
- if File.directory?(settings.defaults.to_s)
113
- Dir["#{settings.defaults}/**/*.rb"].each do |default|
114
- begin
115
- eval File.read(default)
116
- rescue Exception => e
117
- raise "Unable to load default responses from: #{default}"
118
- end
119
- end
120
- end
121
- 200
122
- end
123
- #
124
- put '/mirage/backup' do
125
- MockResponse.backup
126
- 200
127
- end
128
-
129
-
130
- put '/mirage' do
131
- MockResponse.revert
132
- 200
133
- end
134
-
135
- get '/mirage/pid' do
136
- "#{$$}"
137
- end
138
-
139
- error ServerResponseNotFound do
140
- 404
141
- end
142
-
143
- error do
144
- erb request.env['sinatra.error'].message
145
- end
146
-
147
- helpers do
148
-
149
- def response_id
150
- params[:id].to_i
151
- end
152
-
153
- def prime &block
154
- yield Mirage::Client.new "http://localhost:#{settings.port}/mirage"
155
- end
156
-
157
- def send_response(response, body='', request={}, query_string='')
158
- sleep response.delay
159
- content_type(response.content_type)
160
- status response.status
161
- response.value(body, request, query_string)
162
- end
163
- end
164
30
  end
165
31
  end
166
32
 
data/rakefile CHANGED
@@ -13,7 +13,25 @@ end
13
13
  require 'rake'
14
14
  require 'rspec/core/rake_task'
15
15
 
16
- RSpec::Core::RakeTask.new
16
+ task :specs
17
+
18
+ %w(client server).each do |type|
19
+ public_task_name = "#{type}_specs"
20
+ private_task_name = "_#{public_task_name}"
21
+
22
+ RSpec::Core::RakeTask.new(private_task_name) do |task|
23
+ task.pattern = "spec/#{type}/**/*_spec.rb"
24
+ end
25
+
26
+ desc "specs for: #{type}"
27
+ task public_task_name do
28
+ ENV['coverage'] = type
29
+ Rake::Task[private_task_name].invoke
30
+ end
31
+
32
+ Rake::Task["specs"].prerequisites << public_task_name
33
+ end
34
+
17
35
 
18
36
  require 'jeweler'
19
37
  Jeweler::Tasks.new do |gem|
@@ -39,7 +57,7 @@ end
39
57
  Jeweler::RubygemsDotOrgTasks.new
40
58
 
41
59
 
42
- require 'cucumber'
60
+ require 'cucumber'
43
61
  require 'cucumber/rake/task'
44
62
  Cucumber::Rake::Task.new(:features) do |t|
45
63
  t.cucumber_opts = "mode=regression features --format pretty"
@@ -50,7 +68,7 @@ task :clean do |task|
50
68
  puts "cleaning"
51
69
  system "gem uninstall -x mirage"
52
70
  end
53
- Dir['*.gem'].each{|gem| FileUtils.rm_f(gem)}
71
+ Dir['*.gem'].each { |gem| FileUtils.rm_f(gem) }
54
72
  task.reenable
55
73
  end
56
74
 
@@ -62,7 +80,4 @@ task :stop do
62
80
  `RACK_ENV='development' && ruby ./bin/mirage stop`
63
81
  end
64
82
 
65
-
66
-
67
-
68
- task :default => [:spec, :install,:features,:clean]
83
+ task :default => [:specs, :install, :features, :clean]
data/server/app.rb ADDED
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ require 'sinatra'
3
+ require 'mock_response'
4
+ require 'server'
@@ -0,0 +1,15 @@
1
+ require 'ptools'
2
+ module Mirage
3
+ module BinaryDataChecker
4
+ class << self
5
+ def contains_binary_data? string
6
+ tmpfile = Tempfile.new("binary_check")
7
+ tmpfile.write(string)
8
+ tmpfile.close
9
+ binary = File.binary?(tmpfile.path)
10
+ FileUtils.rm(tmpfile.path)
11
+ binary
12
+ end
13
+ end
14
+ end
15
+ end
data/server/helpers.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'tempfile'
2
+ require 'ptools'
3
+ module Mirage
4
+ class Server < Sinatra::Base
5
+ module Helpers
6
+ def convert_raw_required_params raw_requirements
7
+ raw_requirements.collect { |requirement| requirement.split(":") }.inject({}) do |hash, pair|
8
+ parameter, value = pair.collect { |string| string.strip }
9
+ value = convert_value(value)
10
+ hash[parameter] =value; hash
11
+ end
12
+ end
13
+
14
+ def convert_raw_required_body_content_requirements raw_requirements
15
+ raw_requirements.collect do |string|
16
+ string.start_with?("%r{") && string.end_with?("}") ? eval(string) : string
17
+ end
18
+ end
19
+
20
+
21
+
22
+ private
23
+ def convert_value(value)
24
+ value.start_with?("%r{") && value.end_with?("}") ? eval(value) : value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,16 +1,25 @@
1
+ require 'binary_data_checker'
2
+ require 'hashie/mash'
1
3
  module Mirage
2
4
  class ServerResponseNotFound < Exception
3
-
5
+
6
+ end
7
+
8
+ class MalformedResponse < Exception
9
+
4
10
  end
11
+
5
12
  class MockResponse
6
13
  class << self
7
14
 
8
- def find_by_id id
9
- response_set_containing(id).values.find { |response| response.response_id == id } || raise(ServerResponseNotFound)
15
+ def find_by_id(id)
16
+ all.find{|response| response.response_id == id} || raise(ServerResponseNotFound)
10
17
  end
11
18
 
12
19
  def delete(id)
13
- response_set_containing(id).delete_if { |http_method, response| response.response_id == id }
20
+ responses.values.each do |set|
21
+ set.values.each{|responses| responses.delete_if{|response|response.response_id == id}}
22
+ end
14
23
  end
15
24
 
16
25
  def delete_all
@@ -18,6 +27,7 @@ module Mirage
18
27
  @next_id = 0
19
28
  end
20
29
 
30
+ #TODO - this is flakey, make a proper copy
21
31
  def backup
22
32
  snapshot.clear and snapshot.replace(responses.deep_clone)
23
33
  end
@@ -27,63 +37,100 @@ module Mirage
27
37
  end
28
38
 
29
39
  def all
30
- all_responses = []
31
- response_sets.each do |response_set|
32
- response_set.values.each{|response|all_responses << response}
33
- end
34
- all_responses
40
+ responses.values.collect do|response_set|
41
+ response_set.values
42
+ end.flatten
35
43
  end
36
44
 
37
- def find_default(body, http_method, name, query_string)
38
- default_response_sets = find_default_responses(name)
39
-
40
- until default_response_sets.empty?
41
- record = find_in_response_set(body, query_string, default_response_sets.delete_at(0), http_method)
42
- return record if record && record.default?
43
- end
45
+ def find_default(options)
46
+ http_method = options[:http_method].upcase!
47
+ default_responses = subdomains(options[:endpoint]).collect do |domain|
48
+ if(responses_for_domain = responses[domain])
49
+ responses_for_domain[http_method].find_all{|response| response.default?} if responses_for_domain[http_method]
50
+ end
51
+ end.flatten.compact
44
52
 
45
- raise ServerResponseNotFound
53
+ default_responses.find{|response| match?(options,response)} || raise(ServerResponseNotFound)
46
54
  end
47
55
 
48
- def find(body, query_string, name, http_method)
49
- find_in_response_set(body, query_string, responses[name], http_method) || raise(ServerResponseNotFound)
56
+ def subdomains(name)
57
+ domains=[]
58
+ name.split("/").each do |part|
59
+ domains << (domains.last ? "#{domains.last}/#{part}" : part)
60
+ end
61
+ domains.reverse
50
62
  end
51
63
 
52
- def add new_response
53
- response_set = target_response_set(new_response)
64
+ def find(options)
65
+ options[:response_set] = responses[options[:endpoint]]
66
+ find_in_response_set(options) || raise(ServerResponseNotFound)
67
+ end
54
68
 
55
- old_response = response_set.delete(new_response.http_method)
56
- response_set[new_response.http_method] = new_response
57
- new_response.response_id = old_response ? old_response.response_id : next_id
69
+ def add(new_response)
70
+ response_set = responses_for_endpoint(new_response)
71
+ method_specific_responses = response_set[new_response.request_spec['http_method'].upcase]||=[]
72
+ old_response = method_specific_responses.delete_at(method_specific_responses.index(new_response)) if method_specific_responses.index(new_response)
73
+ if old_response
74
+ new_response.response_id = old_response.response_id
75
+ else
76
+ new_response.response_id = next_id
77
+ end
78
+ method_specific_responses<<new_response
58
79
  end
59
80
 
60
81
  private
61
-
62
- def find_in_response_set(body, query_string, response_set, http_method)
82
+ def find_in_response_set(options)
83
+ response_set = options.delete(:response_set)
63
84
  return unless response_set
64
- response_set = response_set[body] || response_set[query_string] || response_set[:basic]
65
- response_set[http_method.upcase] if response_set
66
- end
67
85
 
68
- def response_set_containing id
69
- response_sets.each do |response_set|
70
- return response_set if response_set.find { |key, response| response.response_id == id }
86
+ responses_for_http_method = response_set[options[:http_method].upcase] || []
87
+
88
+ responses = responses_for_http_method.find_all do |stored_response|
89
+ match?(options, stored_response)
71
90
  end
72
- {}
91
+
92
+ responses.sort{|a, b| b.score <=> a.score}.first
93
+
73
94
  end
74
95
 
75
- def response_sets
76
- responses.values.collect { |response_sets| response_sets.values }.flatten
96
+ def match?(options,stored_response)
97
+ parameters = options[:params]
98
+ headers = Hash[options[:headers].collect{|key, value| [key.downcase, value]}]
99
+
100
+ request_spec = stored_response.request_spec
101
+
102
+ match = true
103
+
104
+ {request_spec['parameters'] => parameters,
105
+ request_spec['headers'] => headers}.each do |spec, actual|
106
+ spec.each do |key, value|
107
+ value = interpret_value(value)
108
+ if value.is_a? Regexp
109
+ match = false unless value.match(actual[key])
110
+ else
111
+ match = false unless value == actual[key]
112
+ end
113
+ end
114
+ end
115
+
116
+ request_spec['body_content'].each do |value|
117
+ value = interpret_value(value)
118
+ if value.is_a? Regexp
119
+ match = false unless options[:body] =~ value
120
+ else
121
+ match = false unless options[:body].include?(value)
122
+ end
123
+ end
124
+
125
+ match
77
126
  end
78
127
 
79
- def find_default_responses(name)
80
- matches = responses.keys.find_all { |key| name.index(key) == 0 }.sort { |a, b| b.length <=> a.length }
81
- matches.collect { |key| responses[key] }
128
+ def interpret_value(value)
129
+ value.start_with?("%r{") && value.end_with?("}") ? eval(value) : value
82
130
  end
83
131
 
84
- def target_response_set response
85
- responses_sets = responses[response.name]||={}
86
- responses_sets[response.pattern] ||= {}
132
+ def responses_for_endpoint(response)
133
+ responses[response.name]||={}
87
134
  end
88
135
 
89
136
  def responses
@@ -98,40 +145,57 @@ module Mirage
98
145
  @next_id||= 0
99
146
  @next_id+=1
100
147
  end
101
-
102
148
  end
103
149
 
104
- attr_reader :name
105
- attr_accessor :response_id
150
+ attr_reader :name, :request_spec, :response_spec
151
+ attr_accessor :response_id, :requests_url
152
+
153
+ def initialize name, spec={}
154
+
155
+ request_defaults = JSON.parse({:parameters => {},
156
+ :body_content => [],
157
+ :http_method => 'get',
158
+ :headers => {}}.to_json)
159
+ response_defaults = JSON.parse({:default => false,
160
+ :body => Base64.encode64(''),
161
+ :delay => 0,
162
+ :content_type => "text/plain",
163
+ :status => 200}.to_json)
164
+
165
+ @name = name
166
+ @spec = spec
167
+
168
+ @request_spec = Hashie::Mash.new request_defaults.merge(spec['request']||{})
169
+ @response_spec = Hashie::Mash.new response_defaults.merge(spec['response']||{})
170
+
171
+ @request_spec['headers'] = Hash[@request_spec['headers'].collect{|key, value| [key.downcase, value]}]
172
+ @binary = BinaryDataChecker.contains_binary_data? @response_spec['body']
106
173
 
107
- def initialize name, value,options
108
- @name, @value = name, value
109
- @options = {:pattern => :basic, :http_method => 'GET', :delay => 0.0, :status => 200}.merge(options){|key, old_value, new_value| new_value || old_value}
110
- @options[:http_method].upcase!
111
174
  MockResponse.add self
112
175
  end
113
176
 
114
- def method_missing *args
115
- method_name = args.first
116
- key = method_name.to_s.gsub(/\?$/, '').to_sym
117
- method_name.to_s.end_with?('?') ? 'true' == @options[key] : @options[key]
177
+ def default?
178
+ @response_spec["default"]
118
179
  end
119
180
 
120
- def pattern
121
- @options[:pattern] == :basic ? :basic : /#{@options[:pattern]}/
181
+ def score
182
+ [@request_spec['headers'].values, @request_spec['parameters'].values, @request_spec['body_content']].inject(0) do |score, matchers|
183
+ matchers.inject(score){|matcher_score, value| interpret_value(value).is_a?(Regexp) ? matcher_score+=1 : matcher_score+=2}
184
+ end
122
185
  end
123
186
 
124
- def value(body='', request_parameters={}, query_string='')
125
- return @value if file?
187
+ def value(request_body='', request_parameters={}, query_string='')
188
+ body = Base64.decode64(response_spec['body'])
189
+ return body if @binary
126
190
 
127
- value = @value
191
+ value = body.dup
128
192
  value.scan(/\$\{([^\}]*)\}/).flatten.each do |pattern|
129
193
 
130
194
  if (parameter_match = request_parameters[pattern])
131
195
  value = value.gsub("${#{pattern}}", parameter_match)
132
196
  end
133
197
 
134
- [body, query_string].each do |string|
198
+ [request_body, query_string].each do |string|
135
199
  if (string_match = find_match(string, pattern))
136
200
  value = value.gsub("${#{pattern}}", string_match)
137
201
  end
@@ -141,9 +205,27 @@ module Mirage
141
205
  value
142
206
  end
143
207
 
208
+ def == response
209
+ response.is_a?(MockResponse) && @name == response.send(:eval, "@name") && @request_spec == response.send(:eval, "@request_spec") && @response_spec == response.send(:eval, "@response_spec")
210
+ end
211
+
212
+ def raw
213
+ {:id =>response_id, :endpoint => @name, :requests_url => requests_url, :response => @response_spec, :request => @request_spec}.to_json
214
+ end
215
+
216
+ def binary?
217
+ @binary
218
+ end
219
+
144
220
  private
145
221
  def find_match(string, regex)
146
222
  string.scan(/#{regex}/).flatten.first
147
223
  end
224
+
225
+ def interpret_value(value)
226
+ value.start_with?("%r{") && value.end_with?("}") ? eval(value) : value
227
+ end
228
+
229
+
148
230
  end
149
231
  end