mirage 2.4.2 → 3.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
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