spoofer 0.1.0

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 (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