spoofer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +93 -0
  4. data/Rakefile +25 -0
  5. data/features/checking_requests_were_made.feature +39 -0
  6. data/features/configuring_spoofer_via_http.feature +175 -0
  7. data/features/echoing_request_in_response.feature +76 -0
  8. data/features/logging_requests.feature +13 -0
  9. data/features/resetting_stubs.feature +16 -0
  10. data/features/steps/http_client_steps.rb +60 -0
  11. data/features/steps/logging_steps.rb +3 -0
  12. data/features/steps/shell_steps.rb +10 -0
  13. data/features/steps/spoofer_steps.rb +21 -0
  14. data/features/stubbing_requests_by_method.feature +25 -0
  15. data/features/stubbing_requests_by_path.feature +60 -0
  16. data/features/stubbing_requests_from_a_file.feature +33 -0
  17. data/features/stubbing_requests_with_parameters.feature +28 -0
  18. data/features/support/env.rb +15 -0
  19. data/features/support/hash_key_path.rb +7 -0
  20. data/features/support/http_client.rb +50 -0
  21. data/features/support/spoofer_runner.rb +7 -0
  22. data/features/using_rack_middlewares.feature +31 -0
  23. data/lib/spoofer.rb +104 -0
  24. data/lib/spoofer/api.rb +50 -0
  25. data/lib/spoofer/api/api_request.rb +49 -0
  26. data/lib/spoofer/api/stub.rb +39 -0
  27. data/lib/spoofer/fake_host.rb +102 -0
  28. data/lib/spoofer/fake_host/helpers.rb +11 -0
  29. data/lib/spoofer/fake_host/request_echo.rb +42 -0
  30. data/lib/spoofer/fake_host/stubbed_request.rb +73 -0
  31. data/lib/spoofer/version.rb +3 -0
  32. data/spec/fixtures/import_stubs.spoof +3 -0
  33. data/spec/spec_helper.rb +21 -0
  34. data/spec/spoofer/fake_host_spec.rb +74 -0
  35. data/spec/spoofer/stubbed_request_spec.rb +16 -0
  36. data/spec/support/.keep +0 -0
  37. data/spec/support/matchers.rb +7 -0
  38. data/spec/support/pry.rb +1 -0
  39. data/spec/support/request_helper.rb +7 -0
  40. metadata +233 -0
@@ -0,0 +1,16 @@
1
+ Feature: Resetting stubs
2
+ In order to create a deterministic clean slate at the beginning of my specs
3
+ As a developer
4
+ I want to be able to reset all previously configured request stubs
5
+
6
+ Scenario: Clearing a stubbed request
7
+ Given I have a spoofer specification with:
8
+ """
9
+ Spoofer.mimic(:port => 11988).get("/some/path").returning("Hello World", 201)
10
+ """
11
+ When I evaluate the code:
12
+ """
13
+ Spoofer.reset_all!
14
+ """
15
+ And I make an HTTP GET request to "http://localhost:11988/some/path"
16
+ Then I should receive an HTTP 404 response with an empty body
@@ -0,0 +1,60 @@
1
+ Before do
2
+ @httpclient = HttpClient.new
3
+ end
4
+
5
+ def headers_from_string(string)
6
+ string.split("\n").inject({}) do |headers, header_string|
7
+ headers.tap do |h|
8
+ components = header_string.split(":")
9
+ h[components[0].strip] = components[1].strip
10
+ end
11
+ end
12
+ end
13
+
14
+ When /^I make an HTTP (POST|PUT) request to "([^\"]*)" with the payload:$/ do |http_method, url, payload|
15
+ @httpclient.perform_request_with_payload(url, http_method, payload)
16
+ end
17
+
18
+ When /^I make an HTTP (POST|PUT) request with a "([^\"]*)" content-type to "([^\"]*)" and the payload:$/ do |http_method, content_type, url, payload|
19
+ @httpclient.perform_request_with_payload(url, http_method, payload, :content_type => content_type)
20
+ end
21
+
22
+ When /^I make an HTTP (GET|POST|PUT|DELETE|HEAD) request to "([^\"]*)"$/ do |http_method, url|
23
+ @httpclient.perform_request(url, http_method)
24
+ end
25
+
26
+ When /^I make an HTTP (GET|POST|PUT|DELETE|HEAD) request to "([^\"]*)" with the header "([^\"]*)"$/ do |http_method, url, header|
27
+ @httpclient.perform_request(url, http_method, nil, headers_from_string(header))
28
+ end
29
+
30
+ Then /^I should receive an HTTP (\d+) response with an empty body$/ do |status_code|
31
+ steps %Q{
32
+ Then I should receive an HTTP #{status_code} response with a body matching ""
33
+ }
34
+ end
35
+
36
+ Then /^I should receive an HTTP (\d+) response with a body matching "([^\"]*)"$/ do |status_code, http_body|
37
+ @httpclient.should have_response_with_code_and_body(status_code.to_i, http_body)
38
+ end
39
+
40
+ Then /^I should receive an HTTP (\d+) response with a body containing:$/ do |status_code, http_body|
41
+ @httpclient.should have_response_with_code_and_body(status_code.to_i, http_body)
42
+ end
43
+
44
+ Then /^I should receive an HTTP (\d+) response$/ do |status_code|
45
+ @httpclient.should have_response_with_code(status_code.to_i)
46
+ end
47
+
48
+ Then /^I should receive an HTTP (\d+) response with the value "([^\"]*)" for the header "([^\"]*)"$/ do |status_code, header_value, header_key|
49
+ @httpclient.should have_response_with_code_and_header(status_code.to_i, header_key, header_value)
50
+ end
51
+
52
+ Then /^I should receive an HTTP (\d+) response with the JSON value "([^\"]*)" for the key path "([^\"]*)"$/ do |status, json_value, key_path|
53
+ json = JSON.parse(@httpclient.last_response.to_s)
54
+ json.value_for_key_path(key_path).should == json_value
55
+ end
56
+
57
+ Then /^I should receive an HTTP (\d+) response with the Plist value "([^\"]*)" for the key path "([^\"]*)"$/ do |status, json_value, key_path|
58
+ plist = Plist.parse_xml(@httpclient.last_response.to_s)
59
+ plist.value_for_key_path(key_path).should == json_value
60
+ end
@@ -0,0 +1,3 @@
1
+ Then /^I should see "([^"]*)" written to STDOUT$/ do |output|
2
+ TEST_STDOUT.tap { |io| io.rewind }.read.should include(output)
3
+ end
@@ -0,0 +1,10 @@
1
+ TEMP_FILES = []
2
+
3
+ Given /^the file "([^\"]*)" exists with the contents:$/ do |file_path, string|
4
+ File.open(file_path, "w") { |io| io.write(string) }
5
+ TEMP_FILES << file_path
6
+ end
7
+
8
+ After do
9
+ TEMP_FILES.each { |path| FileUtils.rm(path) if File.exist?(path) }
10
+ end
@@ -0,0 +1,21 @@
1
+ Given /^I have a spoofer specification with:$/ do |string|
2
+ SpooferRunner.new.evaluate(string)
3
+ end
4
+
5
+ Given /^that Spoofer is running and accepting remote configuration on "([^\"]*)"$/ do |api_endpoint|
6
+ Spoofer.mimic(:port => 11988, :remote_configuration_path => api_endpoint)
7
+ end
8
+
9
+ Given /^that Spoofer is running and accepting remote configuration on "([^\"]*)" with the existing stubs:$/ do |api_endpoint, existing_stubs|
10
+ Spoofer.mimic(:port => 11988, :remote_configuration_path => api_endpoint) do
11
+ eval(existing_stubs)
12
+ end
13
+ end
14
+
15
+ When /^I evaluate the code:$/ do |string|
16
+ eval(string)
17
+ end
18
+
19
+ After do
20
+ Spoofer.cleanup!
21
+ end
@@ -0,0 +1,25 @@
1
+ Feature: Stubbing requests by path
2
+ In order to test a range of API endpoints and HTTP verbs
3
+ As a developer
4
+ I want to be able to stub requests to return specific responses depending on the request method
5
+
6
+ Scenario: Stubbing a POST request to return a 201 response
7
+ Given I have a spoofer specification with:
8
+ """
9
+ Spoofer.mimic(:port => 11988).post("/some/path").returning("Hello World", 201)
10
+ """
11
+ When I make an HTTP POST request to "http://localhost:11988/some/path"
12
+ Then I should receive an HTTP 201 response with a body matching "Hello World"
13
+
14
+ Scenario: Stubbing the same path with different responses for GET and POST
15
+ Given I have a spoofer specification with:
16
+ """
17
+ Spoofer.mimic(:port => 11988) do
18
+ get("/some/path").returning("Some Record", 200)
19
+ post("/some/path").returning("Created", 201)
20
+ end
21
+ """
22
+ When I make an HTTP GET request to "http://localhost:11988/some/path"
23
+ Then I should receive an HTTP 200 response with a body matching "Some Record"
24
+ When I make an HTTP POST request to "http://localhost:11988/some/path"
25
+ Then I should receive an HTTP 201 response with a body matching "Created"
@@ -0,0 +1,60 @@
1
+ Feature: Stubbing requests by path
2
+ In order to test my app through its entire stack without depending on an external API
3
+ As a developer
4
+ I want to be able to stub requests to specific paths to return a canned response
5
+
6
+ Scenario: Stubbing a GET request to /some/path and return an empty response
7
+ Given I have a spoofer specification with:
8
+ """
9
+ Spoofer.mimic(:port => 11988).get("/some/path")
10
+ """
11
+ When I make an HTTP GET request to "http://localhost:11988/some/path"
12
+ Then I should receive an HTTP 200 response with an empty body
13
+
14
+ Scenario: Stubbing a GET request to /some/path and returning a non-empty response
15
+ Given I have a spoofer specification with:
16
+ """
17
+ Spoofer.mimic(:port => 11988).get("/some/path").returning("Hello World")
18
+ """
19
+ When I make an HTTP GET request to "http://localhost:11988/some/path"
20
+ Then I should receive an HTTP 200 response with a body matching "Hello World"
21
+
22
+ Scenario: Requesting an un-stubbed path and getting a 404 response
23
+ Given I have a spoofer specification with:
24
+ """
25
+ Spoofer.mimic(:port => 11988).get("/some/path")
26
+ """
27
+ When I make an HTTP GET request to "http://localhost:11988/some/other/path"
28
+ Then I should receive an HTTP 404 response with an empty body
29
+
30
+ Scenario: Stubbing a POST request to /some/path and return an empty response
31
+ Given I have a spoofer specification with:
32
+ """
33
+ Spoofer.mimic(:port => 11988).post("/some/path")
34
+ """
35
+ When I make an HTTP POST request to "http://localhost:11988/some/path"
36
+ Then I should receive an HTTP 200 response with an empty body
37
+
38
+ Scenario: Stubbing a PUT request to /some/path and return an empty response
39
+ Given I have a spoofer specification with:
40
+ """
41
+ Spoofer.mimic(:port => 11988).put("/some/path")
42
+ """
43
+ When I make an HTTP PUT request to "http://localhost:11988/some/path"
44
+ Then I should receive an HTTP 200 response with an empty body
45
+
46
+ Scenario: Stubbing a DELETE request to /some/path and return an empty response
47
+ Given I have a spoofer specification with:
48
+ """
49
+ Spoofer.mimic(:port => 11988).delete("/some/path")
50
+ """
51
+ When I make an HTTP DELETE request to "http://localhost:11988/some/path"
52
+ Then I should receive an HTTP 200 response with an empty body
53
+
54
+ Scenario: Stubbing a HEAD request to /some/path and return an empty response
55
+ Given I have a spoofer specification with:
56
+ """
57
+ Spoofer.mimic(:port => 11988).head("/some/path")
58
+ """
59
+ When I make an HTTP HEAD request to "http://localhost:11988/some/path"
60
+ Then I should receive an HTTP 200 response with an empty body
@@ -0,0 +1,33 @@
1
+ Feature: Stubbing requests from a file
2
+ In order to pre-load Spoofer with a set of stubs
3
+ As a developer
4
+ I want to be able to store my stub configuration in a separate file and load it in at runtime
5
+
6
+ Scenario: Stubbing requests using a file
7
+ Given the file "/tmp/test.spoof" exists with the contents:
8
+ """
9
+ get("/ping") { "pong" }
10
+ """
11
+ And I have a spoofer specification with:
12
+ """
13
+ Spoofer.mimic(:port => 11988) do
14
+ import "/tmp/test.mimic"
15
+ end
16
+ """
17
+ When I make an HTTP GET request to "http://localhost:11988/ping"
18
+ Then I should receive an HTTP 200 response with a body matching "pong"
19
+
20
+ Scenario: Stubbed requests from a file persist even when Spoofer is cleared
21
+ Given the file "/tmp/test.spoof" exists with the contents:
22
+ """
23
+ get("/ping") { "pong" }
24
+ """
25
+ And I have a spoofer specification with:
26
+ """
27
+ Spoofer.mimic(:port => 11988) do
28
+ import "/tmp/test.mimic"
29
+ end
30
+ Spoofer.reset_all!
31
+ """
32
+ When I make an HTTP GET request to "http://localhost:11988/ping"
33
+ Then I should receive an HTTP 200 response with a body matching "pong"
@@ -0,0 +1,28 @@
1
+ Feature: Stubbing requests by path
2
+ In order to test requests that use specific query parameters
3
+ As a developer
4
+ I want to be able to only stub requests that have the correct parameters
5
+
6
+ Scenario: Accepting any parameters to a stubbed path
7
+ Given I have a spoofer specification with:
8
+ """
9
+ Spoofer.mimic(:port => 11988).get("/some/path")
10
+ """
11
+ When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar"
12
+ Then I should receive an HTTP 200 response with an empty body
13
+
14
+ Scenario: Accepting specific parameters and matching
15
+ Given I have a spoofer specification with:
16
+ """
17
+ Spoofer.mimic(:port => 11988).get("/some/path").with_query_parameters("foo" => "bar")
18
+ """
19
+ When I make an HTTP GET request to "http://localhost:11988/some/path?foo=bar"
20
+ Then I should receive an HTTP 200 response with an empty body
21
+
22
+ Scenario: Accepting specific parameters and matching
23
+ Given I have a spoofer specification with:
24
+ """
25
+ Spoofer.mimic(:port => 11988).get("/some/path").with_query_parameters("foo" => "bar")
26
+ """
27
+ When I make an HTTP GET request to "http://localhost:11988/some/path?foo=baz"
28
+ Then I should receive an HTTP 404 response
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[.. .. lib]))
2
+
3
+ TEST_STDOUT = StringIO.new
4
+
5
+ Before do
6
+ if test_proxy = ENV["MIMIC_TEST_PROXY"]
7
+ HttpClient.use_proxy(test_proxy)
8
+ end
9
+
10
+ $stdout = TEST_STDOUT
11
+ end
12
+
13
+ After do
14
+ $stdout = STDOUT
15
+ end
@@ -0,0 +1,7 @@
1
+ class Hash
2
+ def value_for_key_path(key_path_string)
3
+ key_path_string.split(".").inject(self) do |result, key|
4
+ result[key]
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ require 'rest_client'
2
+
3
+ class HttpClient
4
+ attr_reader :last_response
5
+
6
+ def self.use_proxy(proxy)
7
+ RestClient.proxy = proxy
8
+ end
9
+
10
+ def initialize
11
+ @last_response = nil
12
+ end
13
+
14
+ def perform_request(url, method, payload = nil, options={})
15
+ RestClient.send(method.downcase, url, options) do |response, request|
16
+ @last_response = response
17
+ end
18
+ end
19
+
20
+ def perform_request_with_payload(url, method, payload, options={})
21
+ RestClient.send(method.downcase, url, payload, options) do |response, request|
22
+ @last_response = response
23
+ end
24
+ end
25
+
26
+ def has_response_with_code_and_body?(status_code, response_body)
27
+ if @last_response
28
+ return @last_response.code.to_i == status_code && @last_response.to_s == response_body
29
+ end
30
+ end
31
+
32
+ def has_response_with_code?(status_code)
33
+ if @last_response
34
+ @last_response.code.to_i == status_code
35
+ end
36
+ end
37
+
38
+ def has_response_with_code_and_header?(status_code, header_key, header_value)
39
+ if @last_response
40
+ @last_response.code.to_i == status_code &&
41
+ @last_response.headers[beautify_header(header_key)] == header_value
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def beautify_header(header_key)
48
+ header_key.downcase.gsub(/-/, '_').to_sym
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ require 'spoofer'
2
+
3
+ class SpooferRunner
4
+ def evaluate(code_string)
5
+ instance_eval(code_string)
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ Feature: Injecting Rack middleware into the request chain for a stub
2
+ In order to test common scenarios (e.g. authentication)
3
+ As a developer
4
+ I want to be able to use Rack middlewares for certain responses
5
+
6
+ Scenario: Using Rack::Auth to simulate failed authentication
7
+ Given I have a spoofer specification with:
8
+ """
9
+ Spoofer.mimic(:port => 11988) do
10
+ use Rack::Auth::Basic do |username, password|
11
+ end
12
+
13
+ get("/some/path")
14
+ end
15
+ """
16
+ When I make an HTTP GET request to "http://localhost:11988/some/path"
17
+ Then I should receive an HTTP 401 response
18
+
19
+ Scenario: Using Rack::Auth to simulate successful authentication
20
+ Given I have a spoofer specification with:
21
+ """
22
+ Spoofer.mimic(:port => 11988) do
23
+ use Rack::Auth::Basic do |username, password|
24
+ username == 'test' && password == 'pass'
25
+ end
26
+
27
+ get("/some/path")
28
+ end
29
+ """
30
+ When I make an HTTP GET request to "http://test:pass@localhost:11988/some/path"
31
+ Then I should receive an HTTP 200 response
data/lib/spoofer.rb ADDED
@@ -0,0 +1,104 @@
1
+ require 'singleton'
2
+ require 'rack'
3
+ require 'logger'
4
+ require 'json'
5
+ require 'plist'
6
+ require 'socket'
7
+ require 'digest'
8
+ require 'sinatra'
9
+
10
+ module Spoofer
11
+ autoload :API, 'spoofer/api'
12
+ autoload :FakeHost, 'spoofer/fake_host'
13
+
14
+ SPOOFER_DEFAULT_PORT = 11988
15
+
16
+ SPOOFER_DEFAULT_OPTIONS = {
17
+ hostname: 'localhost',
18
+ port: SPOOFER_DEFAULT_PORT,
19
+ remote_configuration_path: nil,
20
+ fork: true,
21
+ log: nil
22
+ }
23
+
24
+ def self.poser(options = {}, &block)
25
+ options = SPOOFER_DEFAULT_OPTIONS.merge(options)
26
+
27
+ host = FakeHost.new(options).tap do |h|
28
+ h.instance_eval(&block) if block_given?
29
+ Server.instance.serve(h, options)
30
+ end
31
+ add_host(host)
32
+ end
33
+
34
+ def self.cleanup!
35
+ Spoofer::Server.instance.shutdown
36
+ end
37
+
38
+ def self.reset_all!
39
+ @hosts.each { |h| h.clear }
40
+ end
41
+
42
+ private
43
+
44
+ def self.add_host(host)
45
+ host.tap { |h| (@hosts ||= []) << h }
46
+ end
47
+
48
+ class Server
49
+ include Singleton
50
+
51
+ def logger
52
+ @logger ||= Logger.new(StringIO.new)
53
+ end
54
+
55
+ def serve(app, options)
56
+ if options[:fork]
57
+ @thread = Thread.fork do
58
+ start_service(app, options)
59
+ end
60
+
61
+ wait_for_service(app.hostname, options[:port])
62
+
63
+ else
64
+ start_service(app, options)
65
+ end
66
+ end
67
+
68
+ def start_service(app, options)
69
+ Rack::Handler::Thin.run(app.url_map, {
70
+ :Port => options[:port],
71
+ :Logger => logger,
72
+ :AccessLog => logger,
73
+ })
74
+ end
75
+
76
+ def shutdown
77
+ Thread.kill(@thread) if @thread
78
+ end
79
+
80
+ # courtesy of http://is.gd/eoYho
81
+
82
+ def listening?(host, port)
83
+ begin
84
+ socket = TCPSocket.new(host, port)
85
+ socket.close unless socket.nil?
86
+ true
87
+ rescue Errno::ECONNREFUSED, SocketError,
88
+ Errno::EBADF, # Windows
89
+ Errno::EADDRNOTAVAIL # Windows
90
+ false
91
+ end
92
+ end
93
+
94
+ def wait_for_service(host, port, timeout = 5)
95
+ start_time = Time.now
96
+
97
+ until listening?(host, port)
98
+ if timeout && (Time.now > (start_time + timeout))
99
+ raise SocketError.new("Socket did not open within #{timeout} seconds")
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end