spoofer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +93 -0
- data/Rakefile +25 -0
- data/features/checking_requests_were_made.feature +39 -0
- data/features/configuring_spoofer_via_http.feature +175 -0
- data/features/echoing_request_in_response.feature +76 -0
- data/features/logging_requests.feature +13 -0
- data/features/resetting_stubs.feature +16 -0
- data/features/steps/http_client_steps.rb +60 -0
- data/features/steps/logging_steps.rb +3 -0
- data/features/steps/shell_steps.rb +10 -0
- data/features/steps/spoofer_steps.rb +21 -0
- data/features/stubbing_requests_by_method.feature +25 -0
- data/features/stubbing_requests_by_path.feature +60 -0
- data/features/stubbing_requests_from_a_file.feature +33 -0
- data/features/stubbing_requests_with_parameters.feature +28 -0
- data/features/support/env.rb +15 -0
- data/features/support/hash_key_path.rb +7 -0
- data/features/support/http_client.rb +50 -0
- data/features/support/spoofer_runner.rb +7 -0
- data/features/using_rack_middlewares.feature +31 -0
- data/lib/spoofer.rb +104 -0
- data/lib/spoofer/api.rb +50 -0
- data/lib/spoofer/api/api_request.rb +49 -0
- data/lib/spoofer/api/stub.rb +39 -0
- data/lib/spoofer/fake_host.rb +102 -0
- data/lib/spoofer/fake_host/helpers.rb +11 -0
- data/lib/spoofer/fake_host/request_echo.rb +42 -0
- data/lib/spoofer/fake_host/stubbed_request.rb +73 -0
- data/lib/spoofer/version.rb +3 -0
- data/spec/fixtures/import_stubs.spoof +3 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/spoofer/fake_host_spec.rb +74 -0
- data/spec/spoofer/stubbed_request_spec.rb +16 -0
- data/spec/support/.keep +0 -0
- data/spec/support/matchers.rb +7 -0
- data/spec/support/pry.rb +1 -0
- data/spec/support/request_helper.rb +7 -0
- metadata +233 -0
data/lib/spoofer/api.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spoofer'
|
2
|
+
|
3
|
+
module Spoofer
|
4
|
+
class API < Sinatra::Base
|
5
|
+
class << self
|
6
|
+
attr_accessor :host
|
7
|
+
end
|
8
|
+
|
9
|
+
def host
|
10
|
+
self.class.host
|
11
|
+
end
|
12
|
+
|
13
|
+
%w{get post put delete head}.each do |verb|
|
14
|
+
post "/#{verb}" do
|
15
|
+
api_request = APIRequest.from_request(request, verb)
|
16
|
+
api_request.setup_stubs_on(host)
|
17
|
+
[201, {"Content-Type" => api_request.request_content_type}, api_request.response]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
post "/multi" do
|
22
|
+
api_request = APIRequest.from_request(request)
|
23
|
+
api_request.setup_stubs_on(host)
|
24
|
+
[201, {"Content-Type" => api_request.request_content_type}, api_request.response]
|
25
|
+
end
|
26
|
+
|
27
|
+
post "/clear" do
|
28
|
+
response_body = self.host.inspect
|
29
|
+
self.host.clear
|
30
|
+
[200, {}, "Cleared stubs: #{response_body}"]
|
31
|
+
end
|
32
|
+
|
33
|
+
get "/ping" do
|
34
|
+
[200, {}, "OK"]
|
35
|
+
end
|
36
|
+
|
37
|
+
get "/debug" do
|
38
|
+
[200, {}, self.host.inspect]
|
39
|
+
end
|
40
|
+
|
41
|
+
get "/requests" do
|
42
|
+
[200, {"Content-Type" => "application/json"}, {"requests" => host.received_requests.map(&:to_hash)}.to_json]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
autoload :ApiRequest, 'spoofer/api/api_request'
|
48
|
+
autoload :Stub, 'spoofer/api/stub'
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spoofer'
|
2
|
+
|
3
|
+
module Spoofer
|
4
|
+
class API::APIRequest
|
5
|
+
attr_reader :request_content_type
|
6
|
+
|
7
|
+
def self.from_request(request, method = nil)
|
8
|
+
case request.content_type
|
9
|
+
when /json/
|
10
|
+
data = JSON.parse(request.body.string)
|
11
|
+
when /plist/
|
12
|
+
data = Plist.parse_xml(request.body.string)
|
13
|
+
else
|
14
|
+
data = JSON.parse(request.body.string)
|
15
|
+
end
|
16
|
+
new(data, method, request.content_type)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(data, method = nil, request_content_type = '')
|
20
|
+
@data = data
|
21
|
+
@method = (method || "GET")
|
22
|
+
@stubs = []
|
23
|
+
@request_content_type = request_content_type
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@data.inspect
|
28
|
+
end
|
29
|
+
|
30
|
+
def response
|
31
|
+
response = {"stubs" => @stubs.map(&:to_hash)}
|
32
|
+
|
33
|
+
case request_content_type
|
34
|
+
when /json/
|
35
|
+
response.to_json
|
36
|
+
when /plist/
|
37
|
+
response.to_plist
|
38
|
+
else
|
39
|
+
response.to_json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup_stubs_on(host)
|
44
|
+
(@data["stubs"] || [@data]).each do |stub_data|
|
45
|
+
@stubs << Stub.new(stub_data, stub_data['method'] || @method).on(host)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Spoofer
|
2
|
+
class API::Stub
|
3
|
+
def initialize(data, method = nil)
|
4
|
+
@data = data
|
5
|
+
@method = method
|
6
|
+
end
|
7
|
+
|
8
|
+
def on(host)
|
9
|
+
host.send(@method.downcase.to_sym, path).returning(body, code, headers).tap do |stub|
|
10
|
+
stub.with_query_parameters(params)
|
11
|
+
stub.echo_request!(echo_format)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def echo_format
|
16
|
+
@data['echo'].to_sym rescue nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def path
|
20
|
+
@data['path'] || '/'
|
21
|
+
end
|
22
|
+
|
23
|
+
def body
|
24
|
+
@data['body'] || ''
|
25
|
+
end
|
26
|
+
|
27
|
+
def code
|
28
|
+
@data['code'] || 200
|
29
|
+
end
|
30
|
+
|
31
|
+
def headers
|
32
|
+
@data['headers'] || {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def params
|
36
|
+
@data['params'] || {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spoofer'
|
2
|
+
|
3
|
+
module Spoofer
|
4
|
+
class FakeHost
|
5
|
+
autoload :Helpers, 'spoofer/fake_host/helpers'
|
6
|
+
autoload :RequestEcho, 'spoofer/fake_host/request_echo'
|
7
|
+
autoload :StubbedRequest, 'spoofer/fake_host/stubbed_request'
|
8
|
+
|
9
|
+
attr_reader :hostname, :url_map
|
10
|
+
attr_accessor :log
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@hostname = options[:hostname]
|
14
|
+
@remote_configuration_path = options[:remote_configuration_path]
|
15
|
+
@log = options[:log]
|
16
|
+
@imports = []
|
17
|
+
clear
|
18
|
+
build_url_map!
|
19
|
+
end
|
20
|
+
|
21
|
+
def received_requests
|
22
|
+
@stubs.select { |s| s.received }
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(path, &block)
|
26
|
+
request("GET", path, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(path, &block)
|
30
|
+
request("POST", path, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def put(path, &block)
|
34
|
+
request("PUT", path, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(path, &block)
|
38
|
+
request("DELETE", path, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def head(path, &block)
|
42
|
+
request("HEAD", path, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def import(path)
|
46
|
+
if File.exists?(path)
|
47
|
+
@imports << path unless @imports.include?(path)
|
48
|
+
instance_eval(File.read(path))
|
49
|
+
else
|
50
|
+
raise "Could not locate file for stub import: #{path}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def call(env)
|
55
|
+
@stubs.each(&:build)
|
56
|
+
@app.call(env)
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear
|
60
|
+
@stubs = []
|
61
|
+
@app = Sinatra.new
|
62
|
+
@app.use(Rack::CommonLogger, self.log) if self.log
|
63
|
+
@app.not_found do
|
64
|
+
[404, {}, ""]
|
65
|
+
end
|
66
|
+
@app.helpers do
|
67
|
+
include Helpers
|
68
|
+
end
|
69
|
+
@imports.each { |file| import(file) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def inspect
|
73
|
+
@stubs.inspect
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def method_missing(method, *args, &block)
|
79
|
+
@app.send(method, *args, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def request(method, path, &block)
|
83
|
+
if block_given?
|
84
|
+
@app.send(method.downcase, path, &block)
|
85
|
+
else
|
86
|
+
@stubs << StubbedRequest.new(@app, method, path)
|
87
|
+
@stubs.last
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_url_map!
|
92
|
+
routes = {'/' => self}
|
93
|
+
|
94
|
+
if @remote_configuration_path
|
95
|
+
API.host = self
|
96
|
+
routes[@remote_configuration_path] = API
|
97
|
+
end
|
98
|
+
|
99
|
+
@url_map = Rack::URLMap.new(routes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Spoofer
|
2
|
+
class FakeHost::RequestEcho
|
3
|
+
def initialize(request)
|
4
|
+
@request = request
|
5
|
+
end
|
6
|
+
|
7
|
+
def response_as(format)
|
8
|
+
content_type = case format
|
9
|
+
when :json, :plist
|
10
|
+
"application/#{format.to_s.downcase}"
|
11
|
+
else
|
12
|
+
"text/plain"
|
13
|
+
end
|
14
|
+
[200, {"Content-Type" => content_type}, to_s(format)]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s(format)
|
18
|
+
case format
|
19
|
+
when :json
|
20
|
+
to_hash.to_json
|
21
|
+
when :plist
|
22
|
+
to_hash.to_plist
|
23
|
+
when :text
|
24
|
+
to_hash.inspect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
{"echo" => {
|
30
|
+
"params" => @request.params,
|
31
|
+
"env" => env_without_rack_and_async_env,
|
32
|
+
"body" => @request.body.read
|
33
|
+
}}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def env_without_rack_and_async_env
|
39
|
+
Hash[*@request.env.select { |key, value| key !~ /^(rack|async)/i }.flatten]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spoofer'
|
2
|
+
|
3
|
+
module Spoofer
|
4
|
+
class FakeHost::StubbedRequest
|
5
|
+
attr_accessor :received
|
6
|
+
|
7
|
+
def initialize(app, method, path)
|
8
|
+
@method, @path = method, path
|
9
|
+
@code = 200
|
10
|
+
@headers = {}
|
11
|
+
@params = {}
|
12
|
+
@body = ""
|
13
|
+
@app = app
|
14
|
+
@received = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
token = "#{@method} #{@path}"
|
19
|
+
Digest::MD5.hexdigest(token)
|
20
|
+
end
|
21
|
+
|
22
|
+
def returning(body, code=200, headers={})
|
23
|
+
tap do
|
24
|
+
@body = body
|
25
|
+
@code = code
|
26
|
+
@headers = headers
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_query_parameters(params)
|
31
|
+
tap do
|
32
|
+
@params = params
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def echo_request!(format=:json)
|
37
|
+
@echo_request_format = format
|
38
|
+
end
|
39
|
+
|
40
|
+
def matches?(request)
|
41
|
+
if @params.any?
|
42
|
+
request.params == @params
|
43
|
+
else
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def matched_response
|
49
|
+
[@code, @headers, @body]
|
50
|
+
end
|
51
|
+
|
52
|
+
def unmatched_response
|
53
|
+
[404, "", {}]
|
54
|
+
end
|
55
|
+
|
56
|
+
def response_for_request(request)
|
57
|
+
if @echo_request_format
|
58
|
+
@body = RequestEcho.new(request).to_s(@echo_request_format)
|
59
|
+
end
|
60
|
+
|
61
|
+
matches?(request) ? matched_response : unmatched_response
|
62
|
+
end
|
63
|
+
|
64
|
+
def build
|
65
|
+
stub = self
|
66
|
+
|
67
|
+
@app.send(@method.downcase, @path) do
|
68
|
+
stub.received = true
|
69
|
+
stub.response_for_request(request)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'spoofer'
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
|
6
|
+
# Load support files
|
7
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
config.tty = true
|
15
|
+
|
16
|
+
# Run specs in random order to surface order dependencies. If you find an
|
17
|
+
# order dependency and want to debug it, you can fix the order by providing
|
18
|
+
# the seed, which is printed after each run.
|
19
|
+
# --seed 1234
|
20
|
+
config.order = 'random'
|
21
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Spoofer::FakeHost do
|
4
|
+
let(:host) { described_class.new(:hostname => "www.example.com") }
|
5
|
+
|
6
|
+
it "should handle stubbed requests" do
|
7
|
+
host.get("/some/path")
|
8
|
+
host.call(request_for("/some/path")).should match_rack_response(200, {}, "")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should handle stubbed requests that return a response" do
|
12
|
+
host.get("/some/path").returning("hello world")
|
13
|
+
host.call(request_for("/some/path")).should match_rack_response(200, {}, "hello world")
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should handle stubbed requests that return a specific HTTP code" do
|
17
|
+
host.get("/some/path").returning("redirecting", 301)
|
18
|
+
host.call(request_for("/some/path")).should match_rack_response(301, {}, "redirecting")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should handle stubbed requests that return specific headers" do
|
22
|
+
host.get("/some/path").returning("redirecting", 301, {"Location" => "http://somewhereelse.com"})
|
23
|
+
host.call(request_for("/some/path")).should match_rack_response(301, {"Location" => "http://somewhereelse.com"}, "redirecting")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not recognize requests if they have the incorrect HTTP method" do
|
27
|
+
host.get("/some/path")
|
28
|
+
host.call(request_for("/some/path", :method => "POST")).should match_rack_response(404, {}, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not handle multiple requests to a path with different HTTP methods" do
|
32
|
+
host.get("/some/path").returning("GET Request", 200)
|
33
|
+
host.post("/some/path").returning("POST Request", 201)
|
34
|
+
host.call(request_for("/some/path", :method => "GET")).should match_rack_response(200, {}, "GET Request")
|
35
|
+
host.call(request_for("/some/path", :method => "POST")).should match_rack_response(201, {}, "POST Request")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should handle requests with behaviour specified in a block using the Sinatra API" do
|
39
|
+
host.get("/some/path") do
|
40
|
+
content_type 'text/plain'
|
41
|
+
'bobby'
|
42
|
+
end
|
43
|
+
host.call(request_for("/some/path", :method => "GET")).should match_rack_response(200, {'Content-Type' => 'text/plain;charset=utf-8'}, 'bobby')
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should allow stubs to be cleared" do
|
47
|
+
host.get("/some/path")
|
48
|
+
host.call(request_for("/some/path")).should match_rack_response(200, {}, "")
|
49
|
+
host.clear
|
50
|
+
host.call(request_for("/some/path")).should match_rack_response(404, {}, "")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should allow stubs to be imported from a file" do
|
54
|
+
host.import(File.join(File.dirname(__FILE__), *%w[.. fixtures import_stubs.spoof]))
|
55
|
+
host.call(request_for("/imported/path")).should match_rack_response(200, {}, "")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not clear imported stubs" do
|
59
|
+
host.import(File.join(File.dirname(__FILE__), *%w[.. fixtures import_stubs.spoof]))
|
60
|
+
host.clear
|
61
|
+
host.call(request_for("/imported/path")).should match_rack_response(200, {}, "")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should raise if import file does not exist" do
|
65
|
+
proc {
|
66
|
+
host.import(File.join(File.dirname(__FILE__), *%w[.. fixtures doesnt_exist.spoof]))
|
67
|
+
}.should raise_error
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns a StubbedRequest" do
|
71
|
+
host.get("/some/path").should be_kind_of(Spoofer::FakeHost::StubbedRequest)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|