Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Making CORS work #2

Merged
merged 15 commits into from
Jan 13, 2015
Merged
16 changes: 13 additions & 3 deletions lib/pact/consumer/mock_service/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
require 'pact/consumer/mock_service/interaction_list'
require 'pact/consumer/mock_service/interaction_delete'
require 'pact/consumer/mock_service/interaction_post'
require 'pact/consumer/mock_service/interaction_options'
require 'pact/consumer/mock_service/interaction_replay'
require 'pact/consumer/mock_service/missing_interactions_get'
require 'pact/consumer/mock_service/verification_get'
require 'pact/consumer/mock_service/log_get'
require 'pact/consumer/mock_service/pact_post'
require 'pact/consumer/mock_service/pact_options'
require 'pact/consumer/mock_service/candidate_options'
require 'pact/support'

AwesomePrint.defaults = {
Expand All @@ -33,14 +36,18 @@ def initialize options = {}
@name = options.fetch(:name, "MockService")
pact_dir = options[:pact_dir]
interactions = []
cors_enabled= options[:cors_enabled]
@handlers = [
MissingInteractionsGet.new(@name, @logger, interaction_list),
VerificationGet.new(@name, @logger, interaction_list, log_description),
InteractionPost.new(@name, @logger, interaction_list),
InteractionDelete.new(@name, @logger, interaction_list),
InteractionOptions.new(@name, @logger),
LogGet.new(@name, @logger),
PactPost.new(@name, @logger, interactions, pact_dir),
InteractionReplay.new(@name, @logger, interaction_list, interactions)
PactOptions.new(@name, @logger),
CandidateOptions.new(@name, @logger, cors_enabled),
InteractionReplay.new(@name, @logger, interaction_list, interactions, cors_enabled),
]
end

Expand All @@ -65,14 +72,17 @@ def call env
response = []
begin
relevant_handler = @handlers.detect { |handler| handler.match? env }
response = add_cors_header(relevant_handler.respond(env))
res= relevant_handler.respond(env)
response= relevant_handler.enable_cors? ? add_cors_header(res) : res
rescue StandardError => e
@logger.error 'Error ocurred in mock service:'
@logger.ap e, :error
puts e.inspect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

puts e.backtrace
@logger.ap e.backtrace
response = [500, {'Content-Type' => 'application/json'}, [{message: e.message, backtrace: e.backtrace}.to_json]]
rescue Exception => e
@logger.error 'Exception ocurred in mock service:'
@logger.error 'Exception occurred in mock service:'
@logger.ap e, :error
@logger.ap e.backtrace
raise e
Expand Down
22 changes: 22 additions & 0 deletions lib/pact/consumer/mock_service/candidate_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'pact/consumer/mock_service/web_request_options'

module Pact
module Consumer

# Allow web preflight requests to the intaractions setup by the user
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intaractions => interactions

# This is only needed in a CORS setup, where the browsers do
# an OPTIONS call before a DELETE, POST (for most request), etc. in a cross domain requests
class CandidateOptions < WebRequestOptions
def initialize name, logger, cors_enabled
super(name,logger)
@cors_enabled = cors_enabled
end

# Will match all requests to OPTIONS when in CORS mode
def request_path_match? env
@cors_enabled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there's no check for request method == 'OPTIONS', what stops every method matching this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following is defined in the parent
def request_method
'OPTIONS'
end

With its own parent having the matcher defined

def match? env
(request_header_match? env) && (request_path_match? env) && (request_method_match? env)
end

end
end
end
end

4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/interaction_delete.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
require 'pact/consumer/mock_service/rack_request_helper'
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer

class InteractionDelete < MockServiceAdministrationEndpoint
class InteractionDelete < WebRequestAdministration

include RackRequestHelper

Expand Down
14 changes: 14 additions & 0 deletions lib/pact/consumer/mock_service/interaction_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'pact/consumer/mock_service/web_request_options'

module Pact
module Consumer

# Allow web preflight requests to Pact infrastructure
# Browsers typically do a OPTIONS before a POST for cross domain requests
class InteractionOptions < WebRequestOptions
def request_path
'/interactions'
end
end
end
end
4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/interaction_post.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer
class InteractionPost < MockServiceAdministrationEndpoint
class InteractionPost < WebRequestAdministration

attr_accessor :interaction_list

Expand Down
7 changes: 6 additions & 1 deletion lib/pact/consumer/mock_service/interaction_replay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ class InteractionReplay

attr_accessor :name, :logger, :interaction_list, :interactions

def initialize name, logger, interaction_list, interactions
def initialize name, logger, interaction_list, interactions, cors_enabled=false
@name = name
@logger = logger
@interaction_list = interaction_list
@interactions = DistinctInteractionsFilter.new(interactions)
@cors_enabled = cors_enabled
end

def match? env
Expand All @@ -39,6 +40,10 @@ def respond env
find_response request_as_hash_from(env)
end

def enable_cors?
@cors_enabled
end

private

def find_response request_hash
Expand Down
4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/log_get.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer
class LogGet < MockServiceAdministrationEndpoint
class LogGet < WebRequestAdministration

include RackRequestHelper

Expand Down
4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/missing_interactions_get.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer

class MissingInteractionsGet < MockServiceAdministrationEndpoint
class MissingInteractionsGet < WebRequestAdministration
include RackRequestHelper

def initialize name, logger, interaction_list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ def initialize name, logger
include RackRequestHelper

def match? env
headers_from(env)['X-Pact-Mock-Service'] &&
env['PATH_INFO'] == request_path &&
env['REQUEST_METHOD'] == request_method
(request_header_match? env) && (request_path_match? env) && (request_method_match? env)
end

def request_path
Expand All @@ -26,6 +24,18 @@ def request_method
raise NotImplementedError
end

private
def request_header_match? env
raise NotImplementedError
end
def request_path_match? env
env['PATH_INFO'] == request_path
end
def request_method_match? env
env['REQUEST_METHOD'] == request_method
end


end
end
end
14 changes: 14 additions & 0 deletions lib/pact/consumer/mock_service/pact_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'pact/consumer/mock_service/web_request_options'

module Pact
module Consumer

# Allow web preflight requests to Pact infrastructure
# Browsers typically do a OPTIONS before a POST for cross domain requests
class PactOptions < WebRequestOptions
def request_path
'/pact'
end
end
end
end
4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/pact_post.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer_contract/consumer_contract_writer'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer
class PactPost < MockServiceAdministrationEndpoint
class PactPost < WebRequestAdministration

attr_accessor :consumer_contract, :interactions, :default_options

Expand Down
1 change: 1 addition & 0 deletions lib/pact/consumer/mock_service/rack_request_helper.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'cgi/core'
module Pact
module Consumer

Expand Down
4 changes: 2 additions & 2 deletions lib/pact/consumer/mock_service/verification_get.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
require 'pact/consumer/mock_service/web_request_administration'

module Pact
module Consumer
class VerificationGet < MockServiceAdministrationEndpoint
class VerificationGet < WebRequestAdministration

include RackRequestHelper
attr_accessor :interaction_list, :log_description
Expand Down
30 changes: 30 additions & 0 deletions lib/pact/consumer/mock_service/web_request_administration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'pact/consumer/mock_service/rack_request_helper'
require 'pact/consumer/mock_service/mock_service_administration_endpoint'

module Pact
module Consumer

# Administration web requests (GET, DELETE, POST, PUT, etc.)
class WebRequestAdministration < MockServiceAdministrationEndpoint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is meant by "WebRequest"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a hard time with names.

I've subclassed the mock requests into:

  • web request options for all the OPTIONS calls. (Pact ones plus the extra ones for the user).
  • web request administration for the rest of the pact stuff.

Why web request? Because the code is handling what is sent by the browser. Happy to take any better name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking "BrowserRequest" might be more accurate then.


def request_path
raise NotImplementedError
end

def request_method
raise NotImplementedError
end

def enable_cors?
true
end

private
def request_header_match? env
headers_from(env)['X-Pact-Mock-Service']
end

end

end
end
51 changes: 51 additions & 0 deletions lib/pact/consumer/mock_service/web_request_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require 'pact/consumer/mock_service/rack_request_helper'
require 'pact/consumer/mock_service/mock_service_administration_endpoint'

module Pact
module Consumer

# Web Request is OPTIONS, which is a preflight brower request made
# before sending the actual POST, DELETE, etc. in CORS cases
class WebRequestOptions < MockServiceAdministrationEndpoint

include RackRequestHelper

def request_path
raise NotImplementedError
end

def request_method
'OPTIONS'
end

def respond env
logger.info "Preflight browser CORS check before sending data okayed (OPTIONS request)"
[200,
{
'Access-Control-Allow-Origin' => '*',
# '*' is not allowed for 'Access-Control-Allow-Headers. We need to echo back what was provided!
'Access-Control-Allow-Headers' => headers_from(env)["Access-Control-Request-Headers"],
'Access-Control-Allow-Methods' => 'DELETE, POST, GET, HEAD, PUT, TRACE, CONNECT'},
["Browser, go ahead and send the actual request"]
]
end

# Access-Control-Domain does not work on OPTIONs requests.
def enable_cors?
false
end

private
# 'X-Pact-Mock-Service' header is set as a normal header in regular requests (PUT, GET, POST, etc.)
# However, browsers set it within Access-Control-Request-Headers in case of OPTIONS request
# (web browsers make an OPTIONS request prior to the normal request in case of CORS request)
# For OPTIONS request, headers are different
def request_header_match? env
headers_from(env)["Access-Control-Request-Headers"].nil? ? false
: headers_from(env)["Access-Control-Request-Headers"].match(/x-pact-mock-service/)
end

end

end
end
2 changes: 2 additions & 0 deletions lib/pact/mock_service/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class CLI < Thor
method_option :port, aliases: "-p", desc: "Port on which to run the service"
method_option :log, aliases: "-l", desc: "File to which to log output"
method_option :quiet, aliases: "-q", desc: "If true, no admin messages will be shown"
method_option :cors_enabled, aliases: "-o", desc: "If true, mocks requests will have access control origin headers set to '*'"

def service
RunStandaloneMockService.call(options)
Expand All @@ -37,6 +38,7 @@ def self.call options
log.sync = true
service_options[:log_file] = log
end
service_options[:cors] = true if options[:cors]

port = options[:port] || FindAPort.available_port
mock_service = Pact::Consumer::MockService.new(service_options)
Expand Down
Loading