├── VERSION ├── Rakefile ├── spec ├── spec.opts ├── support │ ├── fakefs_ext.rb │ ├── clear_fixtures.rb │ ├── time.rb │ └── rack_reflector.rb ├── integration │ ├── white_list_spec.rb │ ├── custom_identifier_spec.rb │ ├── normal_flow_spec.rb │ ├── read_body_compatibility_spec.rb │ ├── sets_spec.rb │ └── unique_fixtures_spec.rb ├── spec_helper.rb ├── ephemeral_response_spec.rb └── ephemeral_response │ ├── configuration_spec.rb │ └── fixture_spec.rb ├── .rvmrc ├── Gemfile ├── .document ├── .gitignore ├── lib ├── ephemeral_response │ ├── request.rb │ ├── cache_service.rb │ ├── certificate.pem │ ├── key.pem │ ├── commands.rb │ ├── configuration.rb │ ├── fixture.rb │ └── proxy.rb └── ephemeral_response.rb ├── examples ├── open_uri_compatibility.rb ├── simple_benchmark.rb ├── custom_cache_key.rb └── white_list.rb ├── MIT_LICENSE ├── ephemeral_response.gemspec ├── History.markdown └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --backtrace 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm gemset use ephemeral_response 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | /.bundle 23 | /Gemfile.lock 24 | -------------------------------------------------------------------------------- /lib/ephemeral_response/request.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module EphemeralResponse 4 | class Request < SimpleDelegator 5 | attr_reader :uri 6 | 7 | undef method 8 | 9 | def initialize(uri, request) 10 | @uri = uri 11 | super request 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/fakefs_ext.rb: -------------------------------------------------------------------------------- 1 | module FakeFS 2 | class Dir 3 | class << self 4 | alias glob_without_block glob 5 | 6 | def glob(pattern, &block) 7 | ary = glob_without_block(pattern) 8 | ary.each &block if block_given? 9 | ary 10 | end 11 | end 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/support/clear_fixtures.rb: -------------------------------------------------------------------------------- 1 | module ClearFixtures 2 | module_function 3 | def clear_fixtures 4 | if Dir.exists?(EphemeralResponse::Configuration.fixture_directory) 5 | FileUtils.rm_rf(EphemeralResponse::Configuration.fixture_directory) 6 | end 7 | EphemeralResponse::Fixture.clear 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/time.rb: -------------------------------------------------------------------------------- 1 | class Time 2 | class << self 3 | alias now_without_travel now 4 | 5 | def travel(moment) 6 | @travel_string = moment.to_s 7 | yield 8 | ensure 9 | @travel_string = nil 10 | end 11 | 12 | def now 13 | if @travel_string 14 | Time.parse(@travel_string, now_without_travel) 15 | else 16 | now_without_travel 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ephemeral_response.rb: -------------------------------------------------------------------------------- 1 | module EphemeralResponse 2 | require 'ephemeral_response/proxy' 3 | autoload :CacheService, 'ephemeral_response/cache_service' 4 | autoload :Commands, 'ephemeral_response/commands' 5 | autoload :Configuration, 'ephemeral_response/configuration' 6 | autoload :Fixture, 'ephemeral_response/fixture' 7 | autoload :Request, 'ephemeral_response/request' 8 | 9 | VERSION = "0.4.0".freeze 10 | 11 | Error = Class.new(StandardError) 12 | 13 | extend Commands 14 | end 15 | -------------------------------------------------------------------------------- /examples/open_uri_compatibility.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift("lib") 2 | require './lib/ephemeral_response' 3 | require 'benchmark' 4 | require 'open-uri' 5 | 6 | EphemeralResponse::Configuration.expiration = 5 7 | EphemeralResponse.activate 8 | 9 | # Run benchmarks against thefuckingweather.com 10 | # The first request takes much longer than the rest 11 | def benchmark_request(number=1) 12 | uri = URI.parse('http://thefuckingweather.com/?RANDLOC=') 13 | time = Benchmark.realtime do 14 | uri.open 15 | end 16 | puts "Request #{number} took #{time} secs" 17 | end 18 | 19 | 5.times {|n| benchmark_request n + 1 } 20 | -------------------------------------------------------------------------------- /examples/simple_benchmark.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift("lib") 2 | require 'rubygems' 3 | require 'benchmark' 4 | require './lib/ephemeral_response' 5 | 6 | EphemeralResponse::Configuration.expiration = 5 7 | EphemeralResponse.activate 8 | 9 | # Run benchmarks against thefuckingweather.com 10 | # The first request takes much longer than the rest 11 | def benchmark_request(number=1) 12 | uri = URI.parse('http://thefuckingweather.com/?RANDLOC=') 13 | time = Benchmark.realtime do 14 | Net::HTTP.get(uri) 15 | end 16 | puts "Request #{number} took #{time} secs" 17 | end 18 | 19 | 5.times {|n| benchmark_request n + 1 } 20 | -------------------------------------------------------------------------------- /lib/ephemeral_response/cache_service.rb: -------------------------------------------------------------------------------- 1 | module EphemeralResponse 2 | class CacheService 3 | attr_accessor :request 4 | 5 | def cached? 6 | Fixture.find(uri, http_request) 7 | end 8 | 9 | def get_cached_response 10 | fixture = Fixture.find(uri, http_request) 11 | fixture.raw_response 12 | end 13 | 14 | def cache(raw_response) 15 | fixture = Fixture.new(uri, http_request) 16 | fixture.raw_response = raw_response 17 | fixture.register 18 | end 19 | 20 | def http_request 21 | request.http_request 22 | end 23 | 24 | def uri 25 | request.uri 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/custom_cache_key.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift("lib") 2 | require 'rubygems' 3 | require './lib/ephemeral_response' 4 | require 'benchmark' 5 | 6 | EphemeralResponse.configure do |config| 7 | config.expiration = 1 8 | config.register('example.com') do |request| 9 | "#{request.uri.host}#{request.method}#{request.path}" 10 | end 11 | end 12 | 13 | EphemeralResponse.activate 14 | 15 | def benchmark_request(number=1) 16 | uri = URI.parse('http://example.com/') 17 | time = Benchmark.realtime do 18 | Net::HTTP.start(uri.host) do |http| 19 | get = Net::HTTP::Get.new('/') 20 | get['Date'] = Time.now.to_s 21 | http.request(get) 22 | end 23 | end 24 | puts "Request #{number} took #{time} secs" 25 | end 26 | 27 | 5.times {|n| benchmark_request n + 1 } 28 | -------------------------------------------------------------------------------- /spec/integration/white_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "White Lists" do 4 | before do 5 | clear_fixtures 6 | end 7 | 8 | context "localhost added to white list" do 9 | let(:uri) { URI.parse('http://localhost:9876/') } 10 | let(:http) { Net::HTTP.new uri.host, uri.port } 11 | let(:get) { Net::HTTP::Get.new '/' } 12 | 13 | before do 14 | EphemeralResponse::Configuration.white_list = "localhost" 15 | end 16 | 17 | it "doesn't save a fixture" do 18 | EphemeralResponse::RackReflector.while_running do 19 | http.start {|h| h.request(get) } 20 | EphemeralResponse::Fixture.load_all.should be_empty 21 | Dir.glob("#{EphemeralResponse::Configuration.fixture_directory}/*").should be_empty 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/ephemeral_response/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICKTCCAZICCQCxJv27YPhTITANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTIwNDA2MTk1MTAxWhcN 5 | MjIwNDA0MTk1MTAxWjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 6 | ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls 7 | b2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKjy90rauaubevAJ 8 | YeyaOJRMejC10IOxGXgNOpqdL9fAloOwLs+5yufRuYO8KngZ4eapQvwDojdGDONm 9 | 2Aojp6NS28onxzRRA8QHu25ImGi+S/fPqH9mnP7qmdNgLqIQDJxIflo3XKrmiXkK 10 | 1P6GP6vBDdWF5GotK/iXhcGtUbuvAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAhST/ 11 | zULfkCcfRPOAiNoixKtzY4w/gtYNOoG4znrEoAJxfMLdV3mZrzeqE29PbPApV/Yx 12 | 8Io772IHE18HTZlHx01K3VyuBDOngrkEyqXxZrekBA00K8b0se1IKpo5s7+LLdrO 13 | QTaau8BfvSiPCuD9syYlhe4MHeKHyEMEqDMeVNk= 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /lib/ephemeral_response/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCo8vdK2rmrm3rwCWHsmjiUTHowtdCDsRl4DTqanS/XwJaDsC7P 3 | ucrn0bmDvCp4GeHmqUL8A6I3RgzjZtgKI6ejUtvKJ8c0UQPEB7tuSJhovkv3z6h/ 4 | Zpz+6pnTYC6iEAycSH5aN1yq5ol5CtT+hj+rwQ3VheRqLSv4l4XBrVG7rwIDAQAB 5 | AoGANUR9sa0qsy+PYFUk+ctaIW/Haso4Vv0kkZRiMNN0fSrsidKnv7jNf6/BNQbD 6 | wSAv+GDPjNO8dn7wm1YWsYOyW6okVrgqle3lF/OBnUS2rUnsZdjgd5seXz+nRY6+ 7 | 9Zi7aV8FPHgDMAKWYilzytNGHtWjCAOtLHB7ejv6nDRUbmECQQDRNer1tHUcV6AJ 8 | FyXvAn193S3Xu3bQuN59q5+v1QJDUcFYmIsg1M3IFQDbzGkD850PpXPfNVHMFnAU 9 | b307dw6xAkEAzrvtMHJKvYyP7DyTGlbFS7xWHN3szSwkHgEUoPGcocODmX/moxEW 10 | WRBXoG7ttSGMYqm9QtxwSRl2065n13nIXwJABXfkUVHLMdd0fmhVfH7TKuQKG7Zx 11 | r5j1b9F5lg36Riov5JHwKQaG7nDmGdio8gp/E3aepbnuDmiTu2UCn/hHsQJATey6 12 | QBuknoQgL9y5WiFA5wZLsz/XpZKw3npryyqnbrYiobZ7OhYTxWiKjxehFDhcEUiH 13 | 5W7wCC3IA4xm6eqmowJBAIgSYoSxWyL1rUF1hIptc4/LJqjMtRjTPCojCjc4k0BP 14 | 5MOvgYFLiBoJyoZKvkmU4hI3SZkoB224v/suSkGUJiw= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/white_list.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift("lib") 2 | require 'rubygems' 3 | require './lib/ephemeral_response' 4 | 5 | # Don't create fixtures for the localhost domain 6 | EphemeralResponse::Configuration.white_list = 'localhost' 7 | 8 | EphemeralResponse::Configuration.expiration = 5 9 | EphemeralResponse.activate 10 | 11 | # Start an HTTP server on port 19876 using netcat 12 | process = IO.popen %(echo "HTTP/1.1 200 OK\n\n" | nc -l 19876) 13 | at_exit { Process.kill :KILL, process.pid } 14 | sleep 1 15 | 16 | # Make a request to the server started above 17 | # No new fixtures are created in spec/fixtures/ephemeral_response/ 18 | uri = URI.parse('http://localhost:19876/') 19 | Net::HTTP.get(uri) 20 | 21 | # Fixtures are still created for Google 22 | uri = URI.parse('http://www.google.com/') 23 | Net::HTTP.get(uri) 24 | 25 | puts "The directory should not contain a fixture for localhost" 26 | puts 27 | dir = File.expand_path(EphemeralResponse::Configuration.fixture_directory) 28 | puts %x(set -x; ls -l #{dir}) 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'net/https' 3 | require 'ephemeral_response' 4 | require 'fakefs/safe' 5 | require 'fakefs/spec_helpers' 6 | require 'rspec/autorun' 7 | require 'debugger' 8 | 9 | Dir.glob("spec/support/*.rb") {|f| require File.expand_path(f, '.')} 10 | 11 | class Net::HTTPResponse 12 | def equality_test 13 | [http_version, code, message, body] 14 | end 15 | 16 | def ==(other) 17 | equality_test == other.equality_test 18 | end 19 | end 20 | 21 | RSpec.configure do |config| 22 | config.color = true 23 | config.include ClearFixtures 24 | 25 | config.before(:suite) do 26 | ClearFixtures.clear_fixtures 27 | end 28 | 29 | config.after(:suite) do 30 | EphemeralResponse.deactivate 31 | ClearFixtures.clear_fixtures 32 | end 33 | 34 | config.before(:each) do 35 | EphemeralResponse.activate 36 | end 37 | 38 | config.after(:each) do 39 | EphemeralResponse::Configuration.reset 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /MIT_LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Sandro Turriate 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /ephemeral_response.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'ephemeral_response' 5 | 6 | Gem::Specification.new do |s| 7 | s.required_rubygems_version = '>= 1.3.6' 8 | 9 | s.version = EphemeralResponse::VERSION.dup 10 | 11 | s.name = 'ephemeral_response' 12 | 13 | s.authors = ['Sandro Turriate', 'Les Hill'] 14 | s.email = 'sandro.turriate@gmail.com' 15 | s.homepage = 'https://github.com/sandro/ephemeral_response' 16 | s.summary = 'Save HTTP responses to give your tests a hint of reality.' 17 | s.description = <<-EOD 18 | Save HTTP responses to give your tests a hint of reality. 19 | Responses are saved into your fixtures directory and are used for subsequent web requests until they expire. 20 | EOD 21 | 22 | s.require_path = 'lib' 23 | 24 | s.files = Dir.glob('lib/**/*') + %w(MIT_LICENSE README.markdown History.markdown Rakefile) 25 | 26 | s.add_development_dependency('rspec', ['>= 2.9.0']) 27 | s.add_development_dependency('fakefs', ['>= 0.4.0']) 28 | s.add_development_dependency('unicorn', ['>= 1.0.0']) 29 | s.add_development_dependency('debugger') 30 | s.add_development_dependency('yard', ['>= 0.7.2']) 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/rack_reflector.rb: -------------------------------------------------------------------------------- 1 | require 'unicorn' 2 | 3 | module EphemeralResponse::RackReflector 4 | class Response 5 | attr_reader :ordered_headers, :rack_input 6 | def initialize(env={}) 7 | @ordered_headers = [] 8 | set_ordered_headers(env) 9 | set_rack_input(env) 10 | end 11 | 12 | def set_ordered_headers(env) 13 | env.each do |key, value| 14 | @ordered_headers << [key, value] if key == key.upcase 15 | end 16 | end 17 | 18 | def headers 19 | Hash[ordered_headers] 20 | end 21 | 22 | def set_rack_input(env) 23 | @rack_input = env['rack.input'].read 24 | end 25 | end 26 | 27 | extend self 28 | 29 | def app 30 | lambda do |env| 31 | [ 200, {"Content-Type" => "application/x-yaml"}, [ Response.new(env).to_yaml ] ] 32 | end 33 | end 34 | 35 | def new_server 36 | http_server = Unicorn::HttpServer.new(app, :listeners => ["0.0.0.0:#{port}"]) 37 | http_server.logger.level = Logger::ERROR 38 | http_server 39 | end 40 | 41 | def port 42 | 9876 || ENV['UNICORN_PORT'] 43 | end 44 | 45 | def server 46 | @server ||= new_server 47 | end 48 | 49 | def start 50 | server.start 51 | end 52 | 53 | def stop 54 | server.stop(false) 55 | end 56 | 57 | def while_running 58 | start 59 | yield 60 | ensure 61 | stop 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/ephemeral_response/commands.rb: -------------------------------------------------------------------------------- 1 | module EphemeralResponse 2 | module Commands 3 | def activate 4 | deactivate 5 | server.start unless server && server.running? 6 | ::Net.module_eval do 7 | if const_defined?(:HTTP) && !const_defined?(:OHTTP) 8 | const_set(:OHTTP, remove_const(:HTTP)) 9 | const_set(:HTTP, Net::ProxyHTTP) 10 | end 11 | end 12 | Fixture.load_all 13 | end 14 | 15 | def server 16 | @server ||= new_server 17 | end 18 | 19 | def new_server 20 | s = ProxyServer.new 21 | s.cache_service = CacheService.new 22 | s 23 | end 24 | 25 | def configure 26 | yield Configuration if block_given? 27 | Configuration 28 | end 29 | 30 | def fixture_set 31 | Configuration.fixture_set 32 | end 33 | 34 | def fixture_set=(name) 35 | Configuration.fixture_set = name 36 | Fixture.load_all 37 | end 38 | 39 | def deactivate 40 | server.stop 41 | ::Net.module_eval do 42 | if const_defined?(:OHTTP) 43 | remove_const(:HTTP) 44 | const_set(:HTTP, remove_const(:OHTTP)) 45 | end 46 | end 47 | end 48 | 49 | def fixtures 50 | Fixture.fixtures 51 | end 52 | 53 | # FIXME: Don't deactivate and reactivate, instead set a flag which ignores 54 | # fixtures entirely. 55 | def live 56 | deactivate 57 | yield 58 | activate 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/integration/custom_identifier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Custom Identifiers' do 4 | let(:uri) { URI.parse("http://localhost:9876/") } 5 | let(:http) { Net::HTTP.new uri.host, uri.port } 6 | let(:post) { Net::HTTP::Post.new '/' } 7 | let(:get) { Net::HTTP::Get.new '/' } 8 | 9 | context "with customization based on request body" do 10 | before do 11 | clear_fixtures 12 | post.set_form_data :name => :joe 13 | EphemeralResponse.configure do |config| 14 | config.register(uri.host) do |request| 15 | request.body.split("=").first 16 | end 17 | end 18 | 19 | EphemeralResponse::RackReflector.while_running do 20 | @post_response = http.start {|h| h.request(post) } 21 | end 22 | end 23 | 24 | it "returns the same fixture when the post data is slightly different" do 25 | post.set_form_data :name => :jane 26 | http.start {|h| h.request(post) }.body.should == @post_response.body 27 | end 28 | end 29 | 30 | context "when the customization doesn't match" do 31 | before do 32 | clear_fixtures 33 | EphemeralResponse.configure do |config| 34 | config.register(uri.host) do |request| 35 | if Net::HTTP::Post === request 36 | request.body.split("=").first 37 | end 38 | end 39 | end 40 | 41 | EphemeralResponse::RackReflector.while_running do 42 | @post_response = http.start {|h| h.request(get) } 43 | end 44 | end 45 | 46 | it "falls back to the default identifier and returns the correct fixture" do 47 | http.start {|h| h.request(get) }.body.should == @post_response.body 48 | end 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/integration/normal_flow_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Normal flow" do 4 | include FakeFS::SpecHelpers 5 | 6 | after do 7 | clear_fixtures 8 | end 9 | 10 | context "example.com" do 11 | let(:uri) { URI.parse('http://example.com/') } 12 | let(:http) { Net::HTTP.new uri.host, uri.port } 13 | 14 | def get 15 | Net::HTTP::Get.new '/' 16 | end 17 | 18 | def send_request(req) 19 | http.start {|h| h.request(req) } 20 | end 21 | 22 | it "generates a fixture, then uses the fixture" do 23 | real_response = send_request(get) 24 | fixture = EphemeralResponse::Fixture.new(uri, get) 25 | File.exists?(fixture.path).should be_true 26 | fixture_response = send_request(get) 27 | real_response.should == fixture_response 28 | end 29 | 30 | it "generates a new fixture when the initial fixture expires" do 31 | send_request(get) 32 | old_fixture = EphemeralResponse::Fixture.find(uri, get) 33 | send_request(get) 34 | Time.travel((Time.now + EphemeralResponse::Configuration.expiration * 2).to_s) do 35 | EphemeralResponse::Fixture.load_all 36 | send_request(get) 37 | end 38 | new_fixture = EphemeralResponse::Fixture.find(uri, get) 39 | old_fixture.created_at.should < new_fixture.created_at 40 | 41 | # use the new fixture 42 | send_request(get) 43 | end 44 | 45 | context "Deactivation" do 46 | it "doesn't create any fixtures" do 47 | EphemeralResponse.deactivate 48 | Net::HTTP.get(uri) 49 | File.exists?(EphemeralResponse::Configuration.fixture_directory).should be_false 50 | end 51 | 52 | it "reactivates" do 53 | EphemeralResponse.deactivate 54 | Net::HTTP.get(uri) 55 | File.exists?(EphemeralResponse::Configuration.fixture_directory).should be_false 56 | EphemeralResponse.activate 57 | Net::HTTP.get(uri) 58 | File.exists?(EphemeralResponse::Configuration.fixture_directory).should be_true 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/integration/read_body_compatibility_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open-uri' 3 | 4 | describe "Read Body Compatibility" do 5 | include FakeFS::SpecHelpers 6 | 7 | after do 8 | clear_fixtures 9 | end 10 | 11 | let(:uri) { URI.parse('http://duckduckgo.com/') } 12 | 13 | def http 14 | Net::HTTP.new uri.host, uri.port 15 | end 16 | 17 | def get 18 | Net::HTTP::Get.new '/' 19 | end 20 | 21 | def new_post 22 | Net::HTTP::Post.new('/') 23 | end 24 | 25 | def send_request(req, body=nil) 26 | http.start {|h| h.request(req, body) } 27 | end 28 | 29 | context "open-uri" do 30 | it "generates a fixture, then uses the fixture" do 31 | real_response = uri.open.read 32 | fixture = EphemeralResponse::Fixture.find(uri, get) 33 | File.exists?(fixture.path).should be_true 34 | fixture_response = send_request(get).body 35 | real_response.should == fixture_response 36 | end 37 | end 38 | 39 | context "Net::HTTP#get" do 40 | it "generates a fixture, then uses the fixture" do 41 | begin 42 | real_response = nil 43 | http.start do |h| 44 | h.request(get) do |r| 45 | r.read_body {|s| real_response = s} 46 | end 47 | end 48 | fixture = EphemeralResponse::Fixture.find(uri, get) 49 | File.exists?(fixture.path).should be_true 50 | fixture_response = send_request(get).body 51 | real_response.should == fixture_response 52 | rescue Exception => e 53 | p e 54 | puts e.backtrace 55 | end 56 | end 57 | end 58 | 59 | context "Net::HTTP.post" do 60 | it "generates a fixture, then uses the fixture" do 61 | post = new_post 62 | post.body = 'foo=bar' 63 | 64 | real_response = nil 65 | http.post('/', 'foo=bar') {|s| real_response = s} 66 | 67 | fixture = EphemeralResponse::Fixture.find(uri, post) 68 | File.exists?(fixture.path).should be_true 69 | 70 | fixture_response = send_request(new_post, 'foo=bar').body 71 | real_response.should == fixture_response 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /History.markdown: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 0.4.0 / 2011-01-18 5 | ---------------- 6 | 7 | #### Fixes 8 | 9 | * Net::HTTP#request now respects the body parameter. When the body 10 | parameter is passed in, it will be set on the request (like normal) 11 | making it available for identification of the fixture. (bernerdschaefer, 12 | veezus) 13 | * Force removal of expired fixtures to overcome missing file exception 14 | * Handle fixture load failures by output errant fixture file to debug output 15 | 16 | 17 | #### Enhancements 18 | 19 | * EphemeralResponse.fixture\_set allows you to keep named groups of fixtures. By 20 | default, this is nil (also :default). 21 | * EphemeralResponse::Configuration.debug\_output prints debugging information to 22 | the provided IO object 23 | 24 | 0.3.2 / 2010-07-30 25 | ------------------ 26 | 27 | #### Fixes 28 | 29 | * Net::HTTP#request now yields the response when a fixture exists 30 | * Net::HTTPResponse#read\_body works when a fixture exists 31 | * OpenURI compatibility (it depends on #read\_body) 32 | 33 | 0.3.1 / 2010-06-29 34 | -------------- 35 | 36 | #### Enhancements 37 | 38 | * Allow custom matchers by host (leshill) 39 | 40 | 0.2.1 / 2010-06-24 41 | -------------- 42 | 43 | #### Enhancements 44 | 45 | * Periods no longer replaced with slashes in fixture names. 46 | * Added skip\expiration option allowing fixtures to never expire. 47 | 48 | 0.2.0 / 2010-06-23 49 | -------------- 50 | 51 | #### Enhancements 52 | 53 | * Fixtures now have use .yml extension instead of .fixture. 54 | * Varying POST data and query strings create new fixtures. Previously, GET / 55 | and GET /?foo=bar resulted in the same fixture. 56 | * Ability to reset configuration with EphemeralResponse::Configuration.reset 57 | * Ability to white list certain hosts. Responses will not be saved for requests 58 | made to hosts in the white list. 59 | Use EphemeralResponse::Configuration.white\_list = "localhost" 60 | * Ephemeral response prints to the Net/HTTP debugger when establishing a 61 | connection. Set http.set\_debug\_output = $stdout to see when Ephemeral 62 | Response connects to a host. 63 | -------------------------------------------------------------------------------- /lib/ephemeral_response/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | module EphemeralResponse 4 | module Configuration 5 | extend self 6 | 7 | attr_accessor :fixture_set 8 | attr_writer :fixture_directory, :skip_expiration 9 | 10 | def effective_directory 11 | if fixture_set.nil? or fixture_set.to_s == 'default' 12 | fixture_directory 13 | else 14 | File.join(fixture_directory, fixture_set.to_s) 15 | end 16 | end 17 | 18 | def fixture_directory 19 | @fixture_directory || "spec/fixtures/ephemeral_response" 20 | end 21 | 22 | # Set an IO object to receive the debugging information. 23 | def debug_output=(io) 24 | if io.respond_to?(:puts) 25 | @debug_output = io 26 | else 27 | raise Error, 'The debug_output object must respond to #puts' 28 | end 29 | end 30 | 31 | def debug_output 32 | @debug_output ||= StringIO.new 33 | end 34 | 35 | def expiration=(expiration) 36 | if expiration.is_a?(Proc) 37 | expiration = instance_eval &expiration 38 | end 39 | @expiration = validate_expiration(expiration) 40 | end 41 | 42 | def expiration 43 | @expiration || one_day 44 | end 45 | 46 | def host_registry 47 | @host_registry ||= Hash.new(proc {}) 48 | end 49 | 50 | def register(host, &block) 51 | host_registry[host] = block 52 | end 53 | 54 | def reset 55 | @fixture_set = nil 56 | @expiration = nil 57 | @fixture_directory = nil 58 | @white_list = nil 59 | @skip_expiration = nil 60 | @host_registry = nil 61 | @debug_output = nil 62 | end 63 | 64 | def skip_expiration 65 | @skip_expiration || false 66 | end 67 | 68 | def white_list 69 | @white_list ||= [] 70 | end 71 | 72 | def white_list=(*hosts) 73 | @white_list = hosts.flatten 74 | end 75 | 76 | protected 77 | 78 | def one_day 79 | 60 * 60 * 24 80 | end 81 | 82 | def validate_expiration(expiration) 83 | raise TypeError, "expiration must be expressed in seconds" unless expiration.is_a?(Fixnum) 84 | expiration 85 | end 86 | 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/ephemeral_response_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe EphemeralResponse do 4 | describe ".activate" do 5 | it "deactivates" do 6 | EphemeralResponse.should_receive(:deactivate) 7 | EphemeralResponse.activate 8 | end 9 | 10 | it "starts the proxy server" do 11 | server = mock(:stop => nil, :running? => false) 12 | EphemeralResponse.stub(:server => server) 13 | server.should_receive(:start) 14 | EphemeralResponse.activate 15 | end 16 | 17 | it "switches Net::HTTP to return a proxied HTTP class" do 18 | EphemeralResponse.deactivate 19 | real_ancestors = Net::HTTP.ancestors 20 | EphemeralResponse.activate 21 | Net::HTTP.ancestors.should include(Net::ProxyHTTP) 22 | Net::OHTTP.ancestors.should == real_ancestors 23 | end 24 | 25 | it "loads all fixtures" do 26 | EphemeralResponse::Fixture.should_receive(:load_all) 27 | EphemeralResponse.activate 28 | end 29 | 30 | end 31 | 32 | describe ".deactivate" do 33 | 34 | it "stops the proxy server" do 35 | server = mock(:stop => nil, :running? => false) 36 | EphemeralResponse.stub(:server => server) 37 | server.should_receive(:stop) 38 | EphemeralResponse.deactivate 39 | end 40 | 41 | it "restores the orignal net http object" do 42 | EphemeralResponse.deactivate 43 | Net::HTTP.ancestors.should_not include(Net::ProxyHTTP) 44 | Net.const_defined?(:OHTTP).should be_false 45 | end 46 | 47 | end 48 | 49 | describe ".fixtures" do 50 | it "returns the registered fixtures" do 51 | EphemeralResponse.fixtures.should == EphemeralResponse::Fixture.fixtures 52 | end 53 | end 54 | 55 | describe ".configure" do 56 | it "yields the configuration object when a block is passed" do 57 | EphemeralResponse.configure {|c| c.expiration = 1} 58 | EphemeralResponse::Configuration.expiration.should == 1 59 | end 60 | 61 | it "returns the configuration object after yielding" do 62 | EphemeralResponse.configure {}.should == EphemeralResponse::Configuration 63 | end 64 | 65 | it "returns the configuration object when no block is present" do 66 | EphemeralResponse.configure.should == EphemeralResponse::Configuration 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/integration/sets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Sets" do 4 | let(:uri) { URI.parse('http://localhost:9876/') } 5 | let(:http) { Net::HTTP.new uri.host, uri.port } 6 | let(:get) { Net::HTTP::Get.new '/' } 7 | 8 | before do 9 | clear_fixtures 10 | end 11 | 12 | context "default set" do 13 | it "saves one fixture to the default directory" do 14 | EphemeralResponse::RackReflector.while_running do 15 | http.start {|h| h.request(get) } 16 | EphemeralResponse::Fixture.fixtures.should_not be_empty 17 | Dir.glob("#{EphemeralResponse::Configuration.fixture_directory}/*").size.should == 1 18 | end 19 | end 20 | 21 | it "restores the fixture" do 22 | clear_fixtures 23 | body = nil 24 | EphemeralResponse::RackReflector.while_running do 25 | body = http.start {|h| h.request(get) } 26 | end 27 | http.start {|h| h.request(get) }.should == body 28 | end 29 | end 30 | 31 | context "named set" do 32 | let(:name) { 'name' } 33 | 34 | describe "#fixture_set=" do 35 | it "unloads the existing fixtures" do 36 | EphemeralResponse::RackReflector.while_running do 37 | http.start {|h| h.request(get) } 38 | EphemeralResponse.fixture_set = name 39 | EphemeralResponse::Fixture.fixtures.should be_empty 40 | end 41 | end 42 | 43 | it "reloads any existing fixtures for the set" do 44 | EphemeralResponse::RackReflector.while_running do 45 | EphemeralResponse.fixture_set = name 46 | http.start {|h| h.request(get) } 47 | end 48 | EphemeralResponse.fixture_set = :default 49 | EphemeralResponse.fixture_set = name 50 | EphemeralResponse::Fixture.find(uri, get).should be 51 | end 52 | end 53 | 54 | it "saves one fixture to the set directory only" do 55 | EphemeralResponse::RackReflector.while_running do 56 | EphemeralResponse.fixture_set = name 57 | http.start {|h| h.request(get) } 58 | EphemeralResponse::Fixture.fixtures.should_not be_empty 59 | File.exists?("#{EphemeralResponse::Configuration.fixture_directory}/#{name}").should be_true 60 | Dir.glob("#{EphemeralResponse::Configuration.fixture_directory}/#{name}/*").size.should == 1 61 | end 62 | end 63 | 64 | it "reads the fixture back from the set directory" do 65 | body = nil 66 | EphemeralResponse::RackReflector.while_running do 67 | EphemeralResponse.fixture_set = name 68 | body = http.start {|h| h.request(get) } 69 | EphemeralResponse::Fixture.fixtures.should_not be_empty 70 | end 71 | http.start {|h| h.request(get) }.should == body 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/ephemeral_response/fixture.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'time' 3 | require 'digest/sha1' 4 | require 'yaml' 5 | require 'stringio' 6 | 7 | module EphemeralResponse 8 | class Fixture 9 | def self.fixtures 10 | @fixtures ||= {} 11 | end 12 | 13 | def self.clear 14 | @fixtures = {} 15 | end 16 | 17 | def self.find(uri, request) 18 | f = Fixture.new(uri, request) 19 | fixtures[f.identifier] 20 | end 21 | 22 | def self.load_all 23 | clear 24 | if File.directory?(Configuration.effective_directory) 25 | Dir.glob("#{Configuration.effective_directory}/*.yml", &method(:load_fixture)) 26 | end 27 | fixtures 28 | end 29 | 30 | def self.load_fixture(file_name) 31 | return unless File.exist?(file_name) 32 | if fixture = YAML.load_file(file_name) 33 | register fixture 34 | else 35 | EphemeralResponse::Configuration.debug_output.puts "EphemeralResponse couldn't load fixture: #{file_name}" 36 | end 37 | end 38 | 39 | def self.find_or_initialize(uri, request, &block) 40 | find(uri, request) || new(uri, request, &block) 41 | end 42 | 43 | def self.register(fixture) 44 | if fixture.expired? 45 | FileUtils.rm_f fixture.path 46 | else 47 | fixtures[fixture.identifier] = fixture 48 | end 49 | end 50 | 51 | attr_accessor :raw_response 52 | attr_reader :uri, :created_at, :raw_request 53 | 54 | def initialize(uri, request) 55 | @uri = uri.normalize 56 | @created_at = Time.now 57 | self.request = request 58 | yield self if block_given? 59 | end 60 | 61 | def request=(request) 62 | if Net::HTTPGenericRequest === request 63 | @request = request 64 | @raw_request = extract_raw_request 65 | else 66 | @raw_request = request 67 | end 68 | end 69 | 70 | def request 71 | @request ||= build_request 72 | end 73 | 74 | def expired? 75 | !Configuration.skip_expiration && (created_at + Configuration.expiration) < Time.now 76 | end 77 | 78 | def file_name 79 | @file_name ||= generate_file_name 80 | end 81 | 82 | def identifier 83 | Digest::SHA1.hexdigest(registered_identifier || default_identifier) 84 | end 85 | 86 | def http_method 87 | request.method 88 | end 89 | 90 | def new? 91 | !self.class.fixtures.has_key?(identifier) 92 | end 93 | 94 | def normalized_name 95 | [uri.host, http_method, fs_path].compact.join("_").tr('/', '-') 96 | end 97 | 98 | def fs_path 99 | uri.path.dup.sub!(/^\/(.+)$/, '\1') 100 | end 101 | 102 | def path 103 | File.join(Configuration.effective_directory, file_name) 104 | end 105 | 106 | def response 107 | s = StringIO.new(raw_response) 108 | b = Net::BufferedIO.new(s) 109 | response = Net::HTTPResponse.read_new(b) 110 | response.reading_body(b, request.response_body_permitted?) {} 111 | response 112 | end 113 | 114 | def register 115 | unless Configuration.white_list.include? uri.host 116 | EphemeralResponse::Configuration.debug_output.puts "#{http_method} #{uri} saved as #{path}" 117 | save 118 | self.class.register self 119 | end 120 | end 121 | 122 | def uri_identifier 123 | if uri.query 124 | parts = uri.to_s.split("?", 2) 125 | parts[1] = parts[1].split('&').sort 126 | parts 127 | else 128 | uri.to_s 129 | end 130 | end 131 | 132 | def to_yaml_properties 133 | %w(@uri @raw_request @raw_response @created_at) 134 | end 135 | 136 | protected 137 | 138 | def build_request 139 | r = ProxyRequest.new(raw_request) 140 | r.http_request 141 | end 142 | 143 | def deep_dup(object) 144 | Marshal.load(Marshal.dump(object)) 145 | end 146 | 147 | def default_identifier 148 | "#{uri_identifier}#{http_method}#{request.body}" 149 | end 150 | 151 | def extract_raw_request 152 | s = StringIO.new 153 | b = Net::BufferedIO.new(s) 154 | request.exec(b, Net::HTTP::HTTPVersion, request.path) 155 | s.rewind 156 | b.read_all 157 | end 158 | 159 | def generate_file_name 160 | "#{normalized_name}_#{identifier[0..6]}.yml" 161 | end 162 | 163 | def registered_identifier 164 | identity = Configuration.host_registry[uri.host].call(Request.new(uri, request)) and identity.to_s 165 | end 166 | 167 | def save 168 | FileUtils.mkdir_p Configuration.effective_directory 169 | File.open(path, 'w') do |f| 170 | f.write to_yaml 171 | end 172 | end 173 | 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/integration/unique_fixtures_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module UniqueRequests 4 | VARIATIONS = %w( 5 | simple_get 6 | get_with_query_string 7 | get_with_query_string_and_basic_auth 8 | get_with_multiple_headers 9 | simple_post 10 | post_with_data 11 | post_with_body 12 | post_with_data_and_query_string 13 | post_with_data_and_query_string_and_basic_auth 14 | post_with_manual_request_body 15 | ) 16 | 17 | def uri 18 | @uri ||= URI.parse "http://localhost:#{EphemeralResponse::RackReflector.port}" 19 | end 20 | 21 | def responses 22 | @responses ||= {} 23 | end 24 | 25 | def set_up_responses 26 | VARIATIONS.each do |request| 27 | response = send(request) 28 | if responses.values.include?(response) 29 | fail "Duplicate response for #{request.inspect}" 30 | else 31 | responses[request] = response 32 | end 33 | end 34 | end 35 | 36 | def perform(request, body=nil) 37 | http = Net::HTTP.new uri.host, uri.port 38 | http.start do |http| 39 | http.request(request, body) 40 | end 41 | end 42 | 43 | def simple_get 44 | perform Net::HTTP::Get.new('/foo') 45 | end 46 | 47 | def get_with_query_string 48 | perform Net::HTTP::Get.new('/?foo=bar&baz=qux') 49 | end 50 | 51 | def get_with_query_string_and_basic_auth 52 | request = Net::HTTP::Get.new('/?foo=bar') 53 | request.basic_auth 'user', 'password' 54 | perform request 55 | end 56 | 57 | def get_with_multiple_headers 58 | request = Net::HTTP::Get.new('/') 59 | request['Accept'] = "application/json, text/html" 60 | request['Accept-Encoding'] = "deflate, gzip" 61 | perform request 62 | end 63 | 64 | def simple_post 65 | perform Net::HTTP::Post.new('/') 66 | end 67 | 68 | def post_with_data 69 | request = Net::HTTP::Post.new('/') 70 | request.set_form_data 'hi' => 'there' 71 | perform request 72 | end 73 | 74 | def post_with_body 75 | request = Net::HTTP::Post.new('/') 76 | request.body = 'post_with=body' 77 | perform request 78 | end 79 | 80 | def post_with_data_and_query_string 81 | request = Net::HTTP::Post.new('/?foo=bar') 82 | request.set_form_data 'post_with' => 'data_and_query_string' 83 | perform request 84 | end 85 | 86 | def post_with_data_and_query_string_and_basic_auth 87 | request = Net::HTTP::Post.new('/?foo=bar') 88 | request.basic_auth 'user', 'password' 89 | request.set_form_data 'post_with' => 'data_and_query_string_and_basic_auth' 90 | perform request 91 | end 92 | 93 | def post_with_manual_request_body 94 | perform Net::HTTP::Post.new('/'), 'post_with=manual_request_body' 95 | end 96 | 97 | end 98 | 99 | describe "Repeated requests properly reloaded" do 100 | include UniqueRequests 101 | 102 | before :all do 103 | EphemeralResponse.activate 104 | clear_fixtures 105 | EphemeralResponse::RackReflector.while_running do 106 | set_up_responses 107 | end 108 | end 109 | 110 | UniqueRequests::VARIATIONS.each do |request| 111 | it "restores the correct response from the fixture" do 112 | send(request).body.should == responses[request].body 113 | end 114 | end 115 | 116 | context "when querystring has different order" do 117 | it "restores the correct response" do 118 | new_response = perform Net::HTTP::Get.new('/?baz=qux&foo=bar') 119 | new_response.body.should == responses['get_with_query_string'].body 120 | end 121 | end 122 | 123 | context "when headers have different order" do 124 | it "restores the correct response when the headers are exactly reversed" do 125 | request = Net::HTTP::Get.new('/') 126 | request['Accept'] = "text/html, application/json" 127 | request['Accept-Encoding'] = "gzip, deflate" 128 | new_response = perform request 129 | new_response.body.should == responses['get_with_multiple_headers'].body 130 | end 131 | 132 | it "restores the correct response when some headers are reversed" do 133 | request = Net::HTTP::Get.new('/') 134 | request['Accept'] = "text/html, application/json" 135 | request['Accept-Encoding'] = "deflate, gzip" 136 | new_response = perform request 137 | new_response.body.should == responses['get_with_multiple_headers'].body 138 | end 139 | end 140 | 141 | context "when the http service has not been started" do 142 | def get 143 | Net::HTTP::Get.new('/foo/bar/baz') 144 | end 145 | 146 | it "restores the correct fixture" do 147 | clear_fixtures 148 | http = Net::HTTP.new uri.host, uri.port 149 | 150 | EphemeralResponse::RackReflector.while_running do 151 | http.request(get) 152 | end 153 | 154 | fixture_uri = uri.dup 155 | fixture_uri.path = get.path 156 | body = http.request(get).body 157 | fixture_body = EphemeralResponse::Fixture.find(fixture_uri, get).response.body 158 | body.should == fixture_body 159 | end 160 | end 161 | 162 | context "when changing the fixture set" do 163 | it "attempts to access the server which is not available" do 164 | EphemeralResponse.fixture_set = :server_down 165 | expect do 166 | simple_get 167 | end.to raise_exception(Errno::ECONNREFUSED, /connection refused/i) 168 | EphemeralResponse.fixture_set = :default 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /spec/ephemeral_response/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EphemeralResponse::Configuration do 4 | subject { EphemeralResponse::Configuration } 5 | 6 | describe ".debug_output=" do 7 | it "raises an exception when the argument isn't an IO object" do 8 | expect do 9 | subject.debug_output = :foo 10 | end.to raise_exception(EphemeralResponse::Error, /must respond to #puts/) 11 | end 12 | 13 | it "stores the argument" do 14 | subject.debug_output = $stderr 15 | subject.instance_variable_get(:@debug_output).should == $stderr 16 | end 17 | end 18 | 19 | describe ".debug_output" do 20 | it "defaults to being off (StringIO)" do 21 | subject.debug_output.should be_instance_of(StringIO) 22 | end 23 | 24 | it "returns the the result of the setter" do 25 | subject.debug_output = $stdout 26 | subject.debug_output.should == $stdout 27 | end 28 | end 29 | 30 | describe "#fixture_set" do 31 | let(:name) { 'name' } 32 | 33 | subject { EphemeralResponse::Configuration.fixture_set } 34 | 35 | it { should be_nil } 36 | 37 | it "can be overwritten" do 38 | EphemeralResponse::Configuration.fixture_set = name 39 | should == name 40 | end 41 | end 42 | 43 | describe "#fixture_directory" do 44 | it "has a default" do 45 | subject.fixture_directory.should == "spec/fixtures/ephemeral_response" 46 | end 47 | 48 | it "can be overwritten" do 49 | subject.fixture_directory = "test/fixtures/ephemeral_response" 50 | subject.fixture_directory.should == "test/fixtures/ephemeral_response" 51 | end 52 | end 53 | 54 | describe "#effective_directory" do 55 | it "defaults to the fixture directory" do 56 | subject.effective_directory.should == "spec/fixtures/ephemeral_response" 57 | end 58 | 59 | context "with a fixture_set" do 60 | before do 61 | subject.fixture_directory = "test/fixtures/ephemeral_response" 62 | subject.fixture_set = :setname 63 | end 64 | 65 | it "adds the fixture_set to the fixture directory" do 66 | subject.effective_directory.should == "test/fixtures/ephemeral_response/setname" 67 | end 68 | 69 | context "that has been reset to the default" do 70 | before do 71 | subject.fixture_set = :default 72 | end 73 | 74 | it "resets to the fixture directory" do 75 | subject.effective_directory.should == "test/fixtures/ephemeral_response" 76 | end 77 | end 78 | end 79 | end 80 | 81 | describe "#expiration" do 82 | it "defaults to 86400" do 83 | subject.expiration.should == 86400 84 | end 85 | 86 | it "can be overwritten" do 87 | subject.expiration = 43200 88 | subject.expiration.should == 43200 89 | end 90 | 91 | context "setting a block" do 92 | it "returns the value of the block" do 93 | subject.expiration = proc { one_day * 7 } 94 | subject.expiration.should == 604800 95 | end 96 | 97 | it "raises an error when the return value of the block is not a FixNum" do 98 | expect do 99 | subject.expiration = proc { "1 day" } 100 | end.to raise_exception(TypeError, "expiration must be expressed in seconds") 101 | end 102 | end 103 | end 104 | 105 | describe "#reset" do 106 | it "resets expiration" do 107 | subject.expiration = 1 108 | subject.expiration.should == 1 109 | subject.reset 110 | 111 | subject.expiration.should == 86400 112 | end 113 | 114 | it "resets fixture_directory" do 115 | subject.fixture_directory = "test/fixtures/ephemeral_response" 116 | subject.fixture_directory.should == "test/fixtures/ephemeral_response" 117 | subject.reset 118 | 119 | subject.fixture_directory.should == "spec/fixtures/ephemeral_response" 120 | end 121 | 122 | it "resets white_list" do 123 | subject.white_list = 'localhost' 124 | subject.white_list.should == ['localhost'] 125 | subject.reset 126 | 127 | subject.white_list.should == [] 128 | end 129 | 130 | it "resets skip_expiration" do 131 | subject.skip_expiration = true 132 | subject.skip_expiration.should == true 133 | subject.reset 134 | 135 | subject.skip_expiration.should == false 136 | end 137 | 138 | it "resets white list after the default has been modified" do 139 | subject.white_list << "localhost" 140 | subject.reset 141 | subject.white_list.should be_empty 142 | end 143 | 144 | it "resets the host_registry" do 145 | subject.register('example.com') {} 146 | subject.reset 147 | subject.host_registry.should be_empty 148 | end 149 | 150 | it "resets debug_output" do 151 | subject.debug_output = $stderr 152 | subject.reset 153 | subject.debug_output.should be_instance_of(StringIO) 154 | end 155 | end 156 | 157 | describe "#white_list" do 158 | it "defaults to an empty array" do 159 | subject.white_list.should == [] 160 | end 161 | 162 | it "allows hosts to be pushed onto the white list" do 163 | subject.white_list << 'localhost' 164 | subject.white_list << 'smackaho.st' 165 | subject.white_list.should == %w(localhost smackaho.st) 166 | end 167 | end 168 | 169 | describe "#white_list=" do 170 | it "sets a single host" do 171 | subject.white_list = 'localhost' 172 | subject.white_list.should == ['localhost'] 173 | end 174 | 175 | it "sets multiple hosts" do 176 | subject.white_list = 'localhost', 'smackaho.st' 177 | subject.white_list.should == ['localhost', 'smackaho.st'] 178 | end 179 | end 180 | 181 | describe "#skip_expiration" do 182 | it "sets skip_expiration to true" do 183 | subject.skip_expiration = true 184 | subject.skip_expiration.should == true 185 | end 186 | 187 | it "sets skip_expiration to false" do 188 | subject.skip_expiration = false 189 | subject.skip_expiration.should == false 190 | end 191 | end 192 | 193 | describe "#register" do 194 | it "registers the block for the host" do 195 | block = Proc.new {} 196 | subject.register('example.com', &block) 197 | subject.host_registry['example.com'].should == block 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Ephemeral Response 2 | ================== 3 | 4 | _Save HTTP responses to give your tests a hint of reality._ 5 | 6 | ## Premise 7 | 8 | Web responses are volatile. Servers go down, API's change, responses change and 9 | every time something changes, your tests should fail. Mocking out web responses 10 | may speed up your test suite but the tests essentially become lies. Ephemeral 11 | Response encourages you to run your tests against real web services while 12 | keeping your test suite snappy by caching the responses and reusing them until 13 | they expire. 14 | 15 | 1. run test suite 16 | 2. all responses are saved to fixtures 17 | 3. disconnect from the network 18 | 4. run test suite 19 | 20 | ## Example 21 | 22 | require 'benchmark' 23 | require 'ephemeral_response' 24 | 25 | EphemeralResponse.activate 26 | 27 | 5.times do 28 | puts Benchmark.realtime { Net::HTTP.get "example.com", "/" } 29 | end 30 | 31 | 1.44242906570435 # First request caches the response as a fixture 32 | 0.000689029693603516 33 | 0.000646829605102539 34 | 0.00064396858215332 35 | 0.000645875930786133 36 | 37 | ## With Rspec 38 | 39 | require 'ephemeral_response' 40 | 41 | Spec::Runner.configure do |config| 42 | 43 | config.before(:suite) do 44 | EphemeralResponse.activate 45 | end 46 | 47 | config.after(:suite) do 48 | EphemeralResponse.deactivate 49 | end 50 | 51 | end 52 | 53 | All responses are cached in yaml files within spec/fixtures/ephemeral\_response. 54 | 55 | I'd recommend git ignoring this directory to ensure your tests always hit the 56 | remote service at least once and to prevent credentials (like API keys) from 57 | being stored in your repo. 58 | 59 | ## Customize how requests get matched by the cache 60 | 61 | Every request gets a unique key that gets added to the cache. Additional 62 | requests attempt to generate this same key so that their responses can be 63 | fetched from the cache. 64 | 65 | The default key is a combination of the URI, request method, and request body. 66 | Occasionally, these properties contain variations which cannot be consistently 67 | reproduced. Time is a good example. If your query string or post data 68 | references the current time then every request will generate a different key 69 | therefore no fixtures will be loaded. You can overcome this issue by 70 | registering a custom key generation block per host. 71 | 72 | An example may help clear this up. 73 | 74 | EphemeralResponse.configure do |config| 75 | config.register('example.com') do |request| 76 | "#{request.uri.host}#{request.method}#{request.path}" 77 | end 78 | end 79 | 80 | # This will get cached 81 | Net::HTTP.start('example.com') do |http| 82 | get = Net::HTTP::Get.new('/') 83 | get['Date'] = Time.now.to_s 84 | http.request(get) 85 | end 86 | 87 | # This is read from the cache even though the date is different 88 | Net::HTTP.start('example.com') do |http| 89 | get = Net::HTTP::Get.new('/') 90 | get['Date'] = "Wed Dec 31 19:00:00 -0500 1969" 91 | http.request(get) 92 | end 93 | 94 | Take a look in `examples/custom_cache_key.rb` to see this in action. 95 | 96 | ## Grouping fixtures to isolate responses 97 | 98 | Occasionally you are in a situation where you are making the same request but 99 | you are expecting a different result, for example, a list of all resources, 100 | create a new resource, and then list them again. For this scenario, you can use 101 | a 'newly_created' fixture set. In your specs, change the fixture set to 102 | 'newly_created' for the create and second list requests. The default fixture 103 | set is named :default (or nil). 104 | 105 | # pseudo code 106 | get('http://example.com/books').should_not contain('The Illiad') 107 | EphemeralResponse.fixture_set = :newly_created 108 | post('http://example.com/books', {:title => 'The Illiad'}) 109 | get('http://example.com/books').should contain('The Illiad') 110 | EphemeralResponse.fixture_set = :default 111 | 112 | ## Configuration 113 | 114 | Change the fixture directory; defaults to "spec/fixtures/ephemeral\_response" 115 | 116 | EphemeralResponse::Configuration.fixture_directory = "test/fixtures/ephemeral_response" 117 | 118 | Change the elapsed time for when a fixture will expire; defaults to 24 hours 119 | 120 | EphemeralResponse::Configuration.expiration = 86400 # 24 hours in seconds 121 | 122 | Pass a block when setting expiration to gain access to the awesome helper 123 | method `one_day` 124 | 125 | EphemeralResponse::Configuration.expiration = lambda do 126 | one_day * 30 # Expire in thirty days: 60 * 60 * 24 * 30 127 | end 128 | 129 | Never let fixtures expire by setting skip\_expiration to true. 130 | 131 | EphemeralResponse::Configuration.skip_expiration = true 132 | 133 | Print debugging information 134 | 135 | EphemeralResponse::Configuration.debug_output = $stderr 136 | 137 | 138 | ### Selenium Tip 139 | 140 | Always allow requests to be made to a host by adding it to the white list. 141 | Helpful when running ephemeral response with selenium which makes requests to 142 | the local server. 143 | 144 | EphemeralResponse::Configuration.white_list = "localhost", "127.0.0.1" 145 | 146 | ### All together now! 147 | 148 | EphemeralResponse.configure do |config| 149 | config.fixture_directory = "test/fixtures/ephemeral_response" 150 | config.expiration = lambda { one_day * 30 } 151 | config.skip_expiration = true 152 | config.white_list = 'localhost' 153 | config.debug_output = $stderr 154 | end 155 | 156 | ## Similar Projects 157 | * [Net Recorder](http://github.com/chrisyoung/netrecorder) 158 | * [Stalefish](http://github.com/jsmestad/stale_fish) 159 | * [VCR](http://github.com/myronmarston/vcr) 160 | 161 | ## Note on Patches/Pull Requests 162 | 163 | * Fork the project. 164 | * Make your feature addition or bug fix. 165 | * Add tests for it. This is important so I don't break it in a 166 | future version unintentionally. 167 | * Commit, do not mess with rakefile, version, or history. 168 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 169 | * Send me a pull request. Bonus points for topic branches. 170 | 171 | ## Copyright 172 | 173 | Copyright (c) 2010 Sandro Turriate. See MIT\_LICENSE for details. 174 | -------------------------------------------------------------------------------- /lib/ephemeral_response/proxy.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'uri' 3 | require 'net/http' 4 | require 'openssl' 5 | require 'thread' 6 | 7 | module Net 8 | 9 | class ProxyHTTP < HTTP 10 | include HTTP::ProxyDelta 11 | @is_proxy_class = true 12 | @proxy_address = 'localhost' 13 | @proxy_port = 44567 14 | 15 | def initialize(*args) 16 | super 17 | self.verify_mode = nil 18 | end 19 | 20 | def verify_mode=(*args) 21 | @verify_mode = OpenSSL::SSL::VERIFY_NONE 22 | end 23 | alias verify_mode verify_mode= 24 | end 25 | 26 | end 27 | 28 | module EphemeralResponse 29 | 30 | class ProxyReader 31 | attr_reader :socket 32 | 33 | def initialize(socket) 34 | @socket = socket 35 | end 36 | 37 | def read 38 | request = catch(:request_done) do 39 | buf = ProxyRequest.new 40 | buf.tunnel_host = socket.tunnel_host if socket.respond_to?(:tunnel_host) 41 | while line = socket.gets 42 | buf << line 43 | if buf.complete? 44 | if buf.content_length? 45 | buf << socket.read(buf.content_length) 46 | end 47 | throw :request_done, buf 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | class ProxyRequest 55 | attr_accessor :tunnel_host 56 | attr_reader :raw 57 | 58 | def initialize(raw = "") 59 | @raw = raw || "" 60 | end 61 | 62 | def <<(str) 63 | raw << str 64 | end 65 | 66 | def complete? 67 | raw =~ /\r?\n\r?\n$/ 68 | end 69 | 70 | def host 71 | uri.host 72 | end 73 | 74 | def port 75 | uri.port 76 | end 77 | 78 | def http_method 79 | @http_method ||= first_line[0] 80 | end 81 | 82 | def uri 83 | @uri ||= parse_uri 84 | end 85 | 86 | def parse_uri 87 | h = first_line[1] 88 | if tunnel_host 89 | h = "https://#{tunnel_host}#{h}" 90 | elsif h !~ /^https?:\/\// 91 | h = "http://#{h}" 92 | end 93 | begin 94 | URI.parse(h) 95 | rescue URI::InvalidURIError 96 | URI.parse(URI.escape(h)) 97 | end 98 | end 99 | 100 | def http_version 101 | first_line[2] 102 | end 103 | 104 | def http_method_class 105 | ::Net::HTTP.const_get(http_method.downcase.capitalize) 106 | end 107 | 108 | def http_request 109 | req = http_method_class.new(uri.request_uri) 110 | req.set_form_data(form_data) unless form_data.empty? 111 | headers.each {|k,v| req[k] = v} 112 | req 113 | end 114 | 115 | def ssl_tunnel? 116 | http_method.upcase == "CONNECT" 117 | end 118 | 119 | def ssl? 120 | uri.scheme == "https" || uri.port == URI::HTTPS::DEFAULT_PORT 121 | end 122 | 123 | def to_s 124 | raw 125 | end 126 | 127 | def first_line 128 | @first_line ||= lines[0].split(" ", 3) 129 | end 130 | 131 | def lines 132 | @lines ||= raw.split(/\r?\n/) 133 | end 134 | 135 | def headers 136 | @headers ||= parse_headers 137 | end 138 | 139 | def parse_headers 140 | h = {} 141 | lines[1..-1].each do |header| 142 | k,v = header.split(": ", 2) 143 | h[k] = v if k && v 144 | end 145 | h 146 | end 147 | 148 | def content_length 149 | headers['Content-Length'].to_i 150 | end 151 | 152 | def content_length? 153 | content_length > 0 154 | end 155 | 156 | def form_data 157 | @form_data ||= parse_form_data 158 | end 159 | 160 | def parse_form_data 161 | h = {} 162 | if content_length? 163 | data = raw.split(/\r?\n\r?\n/, 2).last 164 | data.split("&").each do |set| 165 | k,v = set.split('=', 2) 166 | h[k] = v 167 | end 168 | end 169 | h 170 | end 171 | end 172 | 173 | class ProxyForwarder 174 | attr_reader :proxy_req, :response, :cache_service 175 | 176 | def initialize(proxy_req, cache_service=nil) 177 | @proxy_req = proxy_req 178 | @raw = "" 179 | self.cache_service = cache_service 180 | end 181 | 182 | def cache_service=(cache_service) 183 | if cache_service 184 | @cache_service = cache_service 185 | cache_service.request = proxy_req 186 | cache_service 187 | end 188 | end 189 | 190 | def start 191 | if cached? 192 | yield get_cached_response 193 | else 194 | make_request 195 | cache_response 196 | yield @raw 197 | end 198 | end 199 | 200 | def cached? 201 | if cache_service 202 | cache_service.cached? 203 | end 204 | end 205 | 206 | def cache_response 207 | if cache_service 208 | cache_service.cache(@raw) 209 | end 210 | end 211 | 212 | def get_cached_response 213 | cache_service.get_cached_response 214 | end 215 | 216 | def http_class 217 | Net.const_defined?(:OHTTP) ? Net::OHTTP : Net::HTTP 218 | end 219 | 220 | def make_request 221 | if proxy_req.ssl_tunnel? 222 | @raw = "#{proxy_req.http_version} 200 Connection established\r\n\r\n" 223 | else 224 | http = http_class.new(proxy_req.host, proxy_req.port) 225 | http.use_ssl = proxy_req.ssl? 226 | http.start do |http| 227 | http.request(proxy_req.http_request) do |response| 228 | @response = response 229 | @raw << response_headers 230 | response.read_body do |data| 231 | @raw << data 232 | end 233 | end 234 | end 235 | end 236 | end 237 | 238 | def response_headers 239 | h = ["HTTP/#{response.http_version} #{response.code} #{response.message}"] 240 | response.each_capitalized do |k,v| 241 | unless ['Transfer-Encoding'].include?(k) 242 | h << "#{k}: #{v}" 243 | end 244 | end 245 | h.join("\r\n") << "\r\n\r\n" 246 | end 247 | end 248 | 249 | module SSLTunnel 250 | attr_accessor :tunnel_host 251 | end 252 | 253 | class ProxyServer 254 | Thread.abort_on_exception = true 255 | 256 | attr_reader :ios, :server, :server_thread, :mutex 257 | attr_accessor :port, :cache_service 258 | 259 | def initialize(port=nil) 260 | @ios = [] 261 | @server_thread = [] 262 | @mutex = Mutex.new 263 | @certs = {} 264 | self.port = port || 44567 265 | end 266 | 267 | def dir 268 | File.expand_path(File.dirname(__FILE__)) 269 | end 270 | 271 | def root_ca 272 | @root_ca ||= begin 273 | path = File.join(dir, "root.cer") 274 | OpenSSL::X509::Certificate.new(File.open(path)) 275 | end 276 | end 277 | 278 | def root_key 279 | @root_key ||= begin 280 | path = File.join(dir, "rootkey.pem") 281 | OpenSSL::PKey::RSA.new(File.open(path)) 282 | end 283 | end 284 | 285 | def make_or_get_cert_for(host) 286 | @certs[host] ||= make_cert_for(host) 287 | end 288 | 289 | def make_cert_for(host) 290 | puts "making cert for #{host}" 291 | key = OpenSSL::PKey::RSA.new 2048 292 | cert = OpenSSL::X509::Certificate.new 293 | cert.version = 2 294 | cert.serial = Integer(rand * 1_000_000) 295 | cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-lang/CN=#{host}" 296 | cert.issuer = root_ca.subject # root CA is the issuer 297 | cert.public_key = key.public_key 298 | cert.not_before = Time.now - 60 299 | cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60 # 1 years validity 300 | ef = OpenSSL::X509::ExtensionFactory.new 301 | ef.subject_certificate = cert 302 | ef.issuer_certificate = root_ca 303 | cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false)) 304 | cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth")) 305 | cert.add_extension(ef.create_extension("basicConstraints","CA:FALSE")) 306 | cert.add_extension(ef.create_extension("keyUsage", "keyEncipherment")) 307 | cert.sign(root_key, OpenSSL::Digest::SHA1.new) 308 | [cert, key] 309 | end 310 | 311 | def ssl_sock(sock, host) 312 | cert, key = make_or_get_cert_for(host) 313 | context = OpenSSL::SSL::SSLContext.new 314 | context.cert = cert 315 | context.key = key 316 | ssl = OpenSSL::SSL::SSLSocket.new(sock, context) 317 | ssl.sync_close = true 318 | ssl.extend SSLTunnel 319 | ssl.tunnel_host = host 320 | ssl 321 | end 322 | 323 | def server 324 | @server ||= TCPServer.new(port) 325 | end 326 | 327 | def start 328 | ios << server 329 | @running = true 330 | @stopping = false 331 | @server_thread = Thread.new do 332 | while true 333 | selection = select(ios, [], [], 0.1) 334 | if selection 335 | selection.first.each do |socket_ready| 336 | if socket_ready.closed? 337 | $stderr.puts "#{self.class.name}: socket closed: #{socket_ready.inspect}" 338 | else 339 | handle(socket_ready) 340 | end 341 | end 342 | end 343 | break if @stopping 344 | end 345 | end 346 | self 347 | end 348 | 349 | def running? 350 | @running 351 | end 352 | 353 | def join 354 | server_thread.join 355 | end 356 | 357 | def stop 358 | ios.clear 359 | @stopping = true 360 | @running = false 361 | end 362 | 363 | def handle(socket_ready) 364 | s = socket_ready.accept 365 | reader = ProxyReader.new(s) 366 | request = reader.read 367 | 368 | if request 369 | forwarder = ProxyForwarder.new(request, cache_service) 370 | forwarder.start do |str| 371 | s.print(str) unless s.closed? 372 | end 373 | if request.ssl_tunnel? 374 | ssl = ssl_sock(s, request.host) 375 | mutex.synchronize { ios << ssl } 376 | else 377 | s.close 378 | mutex.synchronize { ios.delete(s) } 379 | end 380 | else 381 | puts "No request #{request.inspect}" 382 | s.close 383 | end 384 | end 385 | end 386 | end 387 | -------------------------------------------------------------------------------- /spec/ephemeral_response/fixture_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EphemeralResponse::Fixture do 4 | include FakeFS::SpecHelpers 5 | 6 | let(:fixture_directory) { File.expand_path EphemeralResponse::Configuration.fixture_directory } 7 | let(:request) { Net::HTTP::Get.new '/' } 8 | let(:uri) { URI.parse("http://example.com/") } 9 | let(:raw_response) { %(HTTP/1.1 200 OK\r\n\r\nRaw Body\r\n) } 10 | let(:fixture) do 11 | EphemeralResponse::Fixture.new(uri, request) do |f| 12 | f.raw_response = raw_response 13 | end 14 | end 15 | 16 | describe ".load_all" do 17 | it "returns the empty fixtures hash when the fixture directory doesn't exist" do 18 | EphemeralResponse::Fixture.should_not_receive :load_fixture 19 | EphemeralResponse::Fixture.load_all.should == {} 20 | end 21 | 22 | it "clears old fixtures" do 23 | EphemeralResponse::Fixture.should_receive(:clear) 24 | EphemeralResponse::Fixture.load_all 25 | end 26 | 27 | context "default set fixture files exist" do 28 | before do 29 | FileUtils.mkdir_p fixture_directory 30 | Dir.chdir(fixture_directory) do 31 | FileUtils.touch %w(1.yml 2.yml) 32 | end 33 | end 34 | 35 | it "calls #load_fixture for each fixture file" do 36 | EphemeralResponse::Fixture.should_receive(:load_fixture).with("#{fixture_directory}/1.yml") 37 | EphemeralResponse::Fixture.should_receive(:load_fixture).with("#{fixture_directory}/2.yml") 38 | EphemeralResponse::Fixture.load_all 39 | end 40 | end 41 | 42 | context "fixture files exist for the set" do 43 | let(:dir) { "#{fixture_directory}/name" } 44 | 45 | before do 46 | EphemeralResponse.fixture_set = :name 47 | FileUtils.mkdir_p dir 48 | Dir.chdir(dir) do 49 | FileUtils.touch %w(1.yml 2.yml) 50 | end 51 | end 52 | 53 | it "calls #load_fixture for each fixture file" do 54 | EphemeralResponse::Fixture.should_receive(:load_fixture).with("#{dir}/1.yml") 55 | EphemeralResponse::Fixture.should_receive(:load_fixture).with("#{dir}/2.yml") 56 | EphemeralResponse::Fixture.load_all 57 | end 58 | end 59 | end 60 | 61 | describe ".load_fixture" do 62 | it "loads the yamlized fixture into the fixtures hash" do 63 | fixture.send :save 64 | EphemeralResponse::Fixture.load_fixture(fixture.path) 65 | EphemeralResponse::Fixture.fixtures.should have_key(fixture.identifier) 66 | end 67 | 68 | it "handles empty yaml files" do 69 | FileUtils.touch '3.yml' 70 | EphemeralResponse::Fixture.load_fixture('3.yml').should be_nil 71 | end 72 | end 73 | 74 | describe ".register" do 75 | context "fixture expired" do 76 | before do 77 | fixture.instance_variable_set(:@created_at, Time.new - (EphemeralResponse::Configuration.expiration * 2)) 78 | fixture.register 79 | end 80 | 81 | it "removes the fixture file" do 82 | File.exists?(fixture.path).should be_false 83 | end 84 | 85 | it "does not add the fixture to the fixtures hash" do 86 | EphemeralResponse::Fixture.fixtures.should_not have_key(fixture.identifier) 87 | end 88 | end 89 | 90 | context "fixture not expired" do 91 | before do 92 | fixture.register 93 | end 94 | 95 | it "adds the the fixture to the fixtures hash" do 96 | EphemeralResponse::Fixture.fixtures[fixture.identifier].should == fixture 97 | end 98 | end 99 | end 100 | 101 | describe ".find" do 102 | context "when fixture registered" do 103 | it "returns the fixture" do 104 | fixture.register 105 | EphemeralResponse::Fixture.find(uri, request).should == fixture 106 | end 107 | end 108 | 109 | context "when fixture not registered" do 110 | it "returns nil" do 111 | EphemeralResponse::Fixture.find(uri, request).should be_nil 112 | end 113 | end 114 | end 115 | 116 | describe ".find_or_initialize" do 117 | context "when the fixture is registered" do 118 | it "returns the registered fixture" do 119 | fixture.register 120 | EphemeralResponse::Fixture.find_or_initialize(uri, request).should == fixture 121 | end 122 | end 123 | 124 | context "when the fixture doesn't exist" do 125 | it "processes the block" do 126 | EphemeralResponse::Fixture.find_or_initialize(uri, request) do |fixture| 127 | fixture.raw_response = raw_response 128 | end.response.body.should == "Raw Body\r\n" 129 | end 130 | 131 | it "returns the new fixture" do 132 | fixture = EphemeralResponse::Fixture.find_or_initialize(uri, request) 133 | EphemeralResponse::Fixture.fixtures[fixture.identifier].should be_nil 134 | end 135 | end 136 | end 137 | 138 | describe "#initialize" do 139 | let(:uri) { URI.parse("HtTP://ExaMplE.Com/") } 140 | subject { EphemeralResponse::Fixture } 141 | 142 | it "normalizes the given uri" do 143 | fixture = subject.new(uri, request) 144 | fixture.uri.should == uri.normalize 145 | end 146 | 147 | it "sets created_at to the current time" do 148 | Time.travel "2010-01-15 10:11:12" do 149 | fixture = subject.new(uri, request) 150 | fixture.created_at.should == Time.parse("2010-01-15 10:11:12") 151 | end 152 | end 153 | 154 | it "yields itself" do 155 | fixture = subject.new(uri, request) do |f| 156 | f.raw_response = raw_response 157 | end 158 | fixture.response.body.should == "Raw Body\r\n" 159 | end 160 | end 161 | 162 | describe "#identifier" do 163 | let(:request) { Net::HTTP::Get.new '/?foo=bar' } 164 | let(:uri) { URI.parse "http://example.com/" } 165 | subject { EphemeralResponse::Fixture.new uri, request } 166 | 167 | context "without a registration for the host" do 168 | it "hashes the uri_identifier with method and the post body" do 169 | Digest::SHA1.should_receive(:hexdigest).with("#{subject.uri_identifier}#{request.method}#{request.body}") 170 | subject.identifier 171 | end 172 | end 173 | 174 | context "with a registration for the host" do 175 | 176 | it "returns a request object with the uri" do 177 | EphemeralResponse.configure {|c| c.register('example.com') {|request| request.uri } } 178 | subject.identifier.should == Digest::SHA1.hexdigest(uri.to_s) 179 | end 180 | 181 | it "returns the hash of the block's return value as a string" do 182 | EphemeralResponse.configure {|c| c.register('example.com') {|request| :identifier } } 183 | subject.identifier.should == Digest::SHA1.hexdigest(:identifier.to_s) 184 | end 185 | 186 | it "returns the hash of the default identifier when the block returns nil" do 187 | EphemeralResponse.configure {|c| c.register('example.com') {|request| } } 188 | subject.identifier.should == Digest::SHA1.hexdigest(subject.send(:default_identifier)) 189 | end 190 | end 191 | 192 | end 193 | 194 | describe "#uri_identifier" do 195 | 196 | it "returns an array containing the host when there is no query string" do 197 | request = Net::HTTP::Get.new '/' 198 | host = URI.parse("http://example.com/") 199 | fixture = EphemeralResponse::Fixture.new(host, request) 200 | fixture.uri_identifier.should == "http://example.com/" 201 | end 202 | 203 | it "does not incorrectly hash different hosts which sort identically" do 204 | request = Net::HTTP::Get.new '/?foo=bar' 205 | host1 = URI.parse("http://a.com/?f=b") 206 | host2 = URI.parse("http://f.com/?b=a") 207 | fixture1 = EphemeralResponse::Fixture.new(host1, request) 208 | fixture2 = EphemeralResponse::Fixture.new(host2, request) 209 | fixture1.uri_identifier.should_not == fixture2.uri_identifier 210 | end 211 | 212 | it "sorts the query strings" do 213 | uri1 = URI.parse("http://example.com/?foo=bar&baz=qux&f") 214 | uri2 = URI.parse("http://example.com/?baz=qux&foo=bar&f") 215 | request1 = Net::HTTP::Get.new uri1.request_uri 216 | request2 = Net::HTTP::Get.new uri2.request_uri 217 | fixture1 = EphemeralResponse::Fixture.new(uri1, request1) 218 | fixture2 = EphemeralResponse::Fixture.new(uri2, request2) 219 | fixture1.uri_identifier.should == fixture2.uri_identifier 220 | end 221 | 222 | it "doesn't mix up the query string key pairs" do 223 | uri1 = URI.parse("http://example.com/?foo=bar&baz=qux") 224 | uri2 = URI.parse("http://example.com/?bar=foo&qux=baz") 225 | request1 = Net::HTTP::Get.new uri1.request_uri 226 | request2 = Net::HTTP::Get.new uri2.request_uri 227 | fixture1 = EphemeralResponse::Fixture.new(uri1, request1) 228 | fixture2 = EphemeralResponse::Fixture.new(uri2, request2) 229 | fixture1.uri_identifier.should_not == fixture2.uri_identifier 230 | end 231 | end 232 | 233 | describe "#register" do 234 | context "uri not white listed" do 235 | it "saves the fixture" do 236 | fixture.register 237 | File.exists?(fixture.path).should be_true 238 | end 239 | 240 | it "registers the fixture" do 241 | fixture.register 242 | EphemeralResponse::Fixture.fixtures.should have_key(fixture.identifier) 243 | end 244 | 245 | it "debugs saving the fixture" do 246 | EphemeralResponse::Configuration.debug_output.should_receive(:puts).with("GET http://example.com/ saved as #{fixture.path}") 247 | fixture.register 248 | end 249 | end 250 | 251 | context "uri is white listed" do 252 | before do 253 | EphemeralResponse::Configuration.white_list << uri.host 254 | end 255 | 256 | it "doesn't save the fixture" do 257 | fixture.register 258 | File.exists?(fixture.path).should be_false 259 | end 260 | 261 | it "doesn't register the fixture" do 262 | fixture.register 263 | EphemeralResponse::Fixture.fixtures.should_not have_key(fixture.identifier) 264 | end 265 | end 266 | end 267 | 268 | describe "#new?" do 269 | it "is new when the fixture hasn't been registered" do 270 | fixture = EphemeralResponse::Fixture.new uri, request 271 | fixture.should be_new 272 | end 273 | 274 | it "isn't new when the fixture has been registered" do 275 | fixture = EphemeralResponse::Fixture.new uri, request 276 | EphemeralResponse::Fixture.register(fixture) 277 | fixture.should_not be_new 278 | end 279 | end 280 | 281 | describe "#file_name" do 282 | it "chops off the starting slash when accessing '/'" do 283 | uri = URI.parse("http://example.com/") 284 | fixture = EphemeralResponse::Fixture.new(uri, request) 285 | fixture.file_name.should =~ /example.com_GET_[\w]{7}.yml/ 286 | end 287 | 288 | it "chops off the starting slash when accessing '/index.html'" do 289 | uri = URI.parse("http://example.com/index.html") 290 | fixture = EphemeralResponse::Fixture.new(uri, request) 291 | fixture.file_name.should =~ /example.com_GET_index.html_[\w]{7}.yml/ 292 | end 293 | 294 | it "looks like on longer paths" do 295 | uri = URI.parse("http://example.com/users/1/photos/1.html") 296 | fixture = EphemeralResponse::Fixture.new(uri, request) 297 | fixture.file_name.should =~ /example.com_GET_users-1-photos-1.html_[\w]{7}.yml/ 298 | end 299 | 300 | describe "#expired?" do 301 | before do 302 | fixture 303 | end 304 | 305 | context "when expiration isn't skipped" do 306 | context "when the expiration time has elapsed" do 307 | it "returns true" do 308 | Time.travel("2030-01-01") do 309 | fixture.should be_expired 310 | end 311 | end 312 | end 313 | 314 | context "when the expiration time has not elapsed" do 315 | it "returns false" do 316 | fixture.should_not be_expired 317 | end 318 | end 319 | end 320 | 321 | context "when expiration is skipped" do 322 | it "returns false" do 323 | fixture 324 | EphemeralResponse::Configuration.skip_expiration = true 325 | Time.travel("2030-01-01") do 326 | fixture.should_not be_expired 327 | end 328 | end 329 | end 330 | end 331 | end 332 | end 333 | --------------------------------------------------------------------------------