mocktopus 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,142 @@
1
+ require 'sinatra'
2
+ require_relative '../../ascii.rb'
3
+
4
+ LOGGER.info 'initialized mocktopus app'
5
+ $input_container = Mocktopus::InputContainer.new
6
+ $mock_api_call_container = Mocktopus::MockApiCallContainer.new
7
+
8
+ error_example = {
9
+ "uri" => "/domain/domain.com/users",
10
+ "headers" => {
11
+ "whitelisting_key_here" => "value"
12
+ },
13
+ "body" => {
14
+ "name" => "the mocktopus",
15
+ "email" => "the_mocktopus@the_mocktopus.com"
16
+ },
17
+ "verb" => "POST",
18
+ "response" => {
19
+ "code" => "202",
20
+ "delay"=> 5000,
21
+ "headers" => {},
22
+ "body" => "Thanks!"
23
+ }
24
+ }
25
+
26
+ post '/mocktopus/inputs/:name' do
27
+ LOGGER.info("received new input named #{params[:name]}")
28
+ begin
29
+ body = JSON.parse(request.body.read().to_s)
30
+ rescue
31
+ status 400
32
+ content_type :json
33
+ error_hash = { 'message' => 'inputs must be created with a valid json descripiton', 'example' => error_example }
34
+ body error_hash.to_json
35
+ return
36
+ end
37
+ LOGGER.debug("body: #{body.inspect()}")
38
+
39
+ LOGGER.debug("creating response object from #{body['response']}")
40
+ response = Mocktopus::Response.new(body['response'])
41
+
42
+ LOGGER.debug("creating input object from #{body.inspect}, #{response.inspect}")
43
+ input = Mocktopus::Input.new(body, response)
44
+
45
+ LOGGER.info("added input #{params[:name]} successfully")
46
+ $input_container.add(params[:name], input)
47
+ end
48
+
49
+ get '/mocktopus/inputs' do
50
+ LOGGER.info("all inputs requested")
51
+ all_inputs = $input_container.all()
52
+ LOGGER.debug("found #{all_inputs.size()} inputs")
53
+ return_inputs = {}
54
+ all_inputs.each do |k,v|
55
+ return_inputs[k] = v.to_hash
56
+ end
57
+ content_type :json
58
+ return_inputs.to_json
59
+ end
60
+
61
+ get '/mocktopus/inputs/:name' do
62
+ LOGGER.info("retrieving input by name #{params[:name]}")
63
+ input = $input_container.get_by(params[:name])
64
+ if (input != nil)
65
+ input.to_hash.to_json
66
+ else
67
+ status 405
68
+ body "input not found"
69
+ end
70
+ end
71
+
72
+ get '/mocktopus/mock_api_calls' do
73
+ $mock_api_call_container.all.to_json
74
+ end
75
+
76
+ delete '/mocktopus/mock_api_calls' do
77
+ $mock_api_call_container.delete_all
78
+ status 200
79
+ body "success"
80
+ end
81
+
82
+ delete '/mocktopus/inputs' do
83
+ LOGGER.info("deleting all inputs")
84
+ $input_container.delete_all()
85
+ end
86
+
87
+ delete '/mocktopus/inputs/:name' do
88
+ LOGGER.info("deleting input by name #{params[:name]}")
89
+ $input_container.delete_by(params[:name])
90
+ end
91
+
92
+ # # # catch all for 404s
93
+ not_found do
94
+ path = request.path
95
+ verb = request.request_method
96
+ request_headers = env.inject({}){|acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc}
97
+ unless(verb.downcase == "get" || verb.downcase == "delete")
98
+ request_headers.merge!({ "content_type" => request.content_type})
99
+ end
100
+ body = request.body.read.to_s
101
+ log_path = path
102
+ unless (request.env['rack.request.query_hash'].nil? || request.env['rack.request.query_hash'].empty?)
103
+ log_path += '?'
104
+ request.env['rack.request.query_hash'].keys.each do |k|
105
+ log_path += k
106
+ log_path += '='
107
+ log_path += request.env['rack.request.query_hash'][k]
108
+ log_path += '&' unless k == request.env['rack.request.query_hash'].keys.last
109
+ end
110
+ end
111
+
112
+ $mock_api_call_container.add(Mocktopus::MockApiCall.new(log_path, verb, request_headers, body))
113
+
114
+ LOGGER.info("not_found catch all invoked")
115
+ start_time = Time.now.to_f
116
+ LOGGER.debug("looking for a match with path #{request.fullpath}, verb #{verb}, headers #{request_headers}, body #{body}")
117
+ match = $input_container.match(path, verb, request_headers, body, request.env['rack.request.query_hash'])
118
+ LOGGER.debug("match lookup complete")
119
+ if (match.nil?)
120
+ LOGGER.info("match not found. sending 428")
121
+ status 428
122
+ content_type :json
123
+ body_detail = {
124
+ 'message' => 'match not found',
125
+ 'call' => {
126
+ 'path' => request.fullpath,
127
+ 'verb' => verb,
128
+ 'headers' => request_headers,
129
+ 'body' => body
130
+ }
131
+ }
132
+ body body_detail.to_json
133
+ else
134
+ LOGGER.info("match found #{match.inspect}")
135
+ sleep(match.response.delay/1000)
136
+ status match.response.code
137
+ headers match.response.headers
138
+ body match.response.body
139
+ end
140
+ end_time = Time.now.to_f
141
+ LOGGER.info("not_found catch all completed in #{((end_time - start_time) * 1000)} milliseconds")
142
+ end
@@ -0,0 +1,31 @@
1
+ require 'thor'
2
+
3
+ module Mocktopus
4
+ class CLI < Thor
5
+ include Thor::Actions
6
+
7
+ desc "start", "starts the mocktopus"
8
+ method_option :port, :desc => "specifies the port to run the mocktopus"
9
+ def start(*args)
10
+ port_option = args.include?('-p') ? '' : ' -p 8081'
11
+ args = args.join(' ')
12
+ command = "bundle exec thin -R #{ENV['CONFIG_RU'] || 'config.ru'} start#{port_option} #{args}"
13
+ run_command(command)
14
+ end
15
+
16
+ desc "stop", "stops the mocktopus"
17
+ def stop
18
+ command = "bundle exec thin stop"
19
+ run_command(command)
20
+ end
21
+
22
+ map 's' => :start
23
+
24
+ private
25
+
26
+ def run_command(command)
27
+ system(command)
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,107 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'rack'
5
+
6
+ module Mocktopus
7
+
8
+ class Input
9
+
10
+ class Error < Exception
11
+ end
12
+
13
+ class ValidationError < Error
14
+ end
15
+
16
+ attr_accessor :uri,
17
+ :headers,
18
+ :body,
19
+ :url_parameters,
20
+ :verb,
21
+ :response
22
+
23
+ def initialize(hash, response)
24
+
25
+ uri_object = nil
26
+ begin
27
+ uri_object = URI.parse(hash['uri'])
28
+ rescue URI::InvalidURIError
29
+ begin
30
+ uri_object = URI.parse(URI::encode(hash['uri']))
31
+ rescue URI::InvalidURIError
32
+ raise ValidationError, "Input uri \"#{hash['uri']}\" is not a valid uri"
33
+ end
34
+ end
35
+
36
+ @uri = uri_object.path
37
+ @url_parameters = Rack::Utils.parse_nested_query(uri_object.query)
38
+ @headers = hash['headers']
39
+ #need to transform body from inferred json/hash to string (if applicable)
40
+ body = ''
41
+ begin
42
+ body = JSON.pretty_generate(hash['body'])
43
+ rescue
44
+ body = hash['body']
45
+ end
46
+ @body = body
47
+ @verb = hash['verb'].upcase
48
+ @response = response
49
+
50
+ validate_instance_variables
51
+
52
+ LOGGER.debug("initialized input object from hash #{hash.inspect()}")
53
+ end
54
+
55
+ def to_hash
56
+ uri_with_parameters = ''
57
+ if(@url_parameters != nil && @url_parameters.length > 0)
58
+ @url_parameters.each do |k,v|
59
+ if (uri_with_parameters.empty?)
60
+ uri_with_parameters ="#{@uri}?"
61
+ else
62
+ uri_with_parameters = "#{uri_with_parameters}&"
63
+ end
64
+ uri_with_parameters = "#{uri_with_parameters}#{k}=#{v}"
65
+ end
66
+ else
67
+ uri_with_parameters = @uri
68
+ end
69
+
70
+ {
71
+ 'uri' => uri_with_parameters,
72
+ 'verb' => @verb,
73
+ 'headers' => @headers,
74
+ 'body' => @body,
75
+ 'response' => @response.to_hash
76
+ }
77
+ end
78
+
79
+ def to_s
80
+ self.to_hash.to_json
81
+ end
82
+
83
+ private
84
+ def validate_instance_variables
85
+
86
+ unless @headers.nil?
87
+ @headers.each do |key, value|
88
+ unless String.eql? key.class and String.eql? value.class
89
+ raise ValidationError, "\"#{key}\" => \"#{value}\" is not a valid header"
90
+ end
91
+ end
92
+ end
93
+
94
+ unless @response.nil?
95
+ unless Mocktopus::Response.eql? @response.class
96
+ raise ValidationError, "\"#{@response}\" is not a response object"
97
+ end
98
+ end
99
+
100
+ unless %w{ OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT PATCH}.include? @verb
101
+ raise ValidationError, "\"#{@verb}\" is not a valid verb"
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,90 @@
1
+ require 'json'
2
+
3
+ module Mocktopus
4
+
5
+ class InputContainer
6
+
7
+ def initialize
8
+ @inputs = {}
9
+ end
10
+
11
+ def add(name, input)
12
+ @inputs[name] = input
13
+ end
14
+
15
+ def get_by(name)
16
+ @inputs[name]
17
+ end
18
+
19
+ def all
20
+ @inputs
21
+ end
22
+
23
+ def delete_all
24
+ @inputs = {}
25
+ end
26
+
27
+ def delete_by(name)
28
+ @inputs.delete(name)
29
+ end
30
+
31
+ def delete_by_input(input)
32
+ first_match = @inputs.select{|k, v| v == input}.keys.first
33
+ self.delete_by(first_match)
34
+ end
35
+
36
+ def match(path, verb, headers, body, url_parameters)
37
+ self.find_match(path, verb, headers, body, url_parameters, @inputs.values)
38
+ end
39
+
40
+ def find_match(path, verb, headers, body, url_parameters, inputs)
41
+ result = nil
42
+ matches = inputs.select{|v| URI.decode(v.uri).eql?(URI.decode(path)) &&
43
+ headers_match?(headers, v.headers) &&
44
+ bodies_match?(v.body, body) &&
45
+ v.verb.eql?(verb) &&
46
+ v.url_parameters.eql?(url_parameters) }
47
+ case matches.size
48
+ when 0
49
+ result = nil
50
+ when 1
51
+ result = matches.first
52
+ else
53
+ result = pop_first_input(matches)
54
+ end
55
+ return result
56
+ end
57
+
58
+ private
59
+ def headers_match?(input_headers, match_headers)
60
+ match = true
61
+ if (match_headers != nil)
62
+ match_headers.each do |k,v|
63
+ if(input_headers[k.downcase.gsub("-", "_")].nil?)
64
+ match = false
65
+ elsif !input_headers[k.downcase.gsub("-", "_")].eql? v
66
+ match = false
67
+ end
68
+ end
69
+ end
70
+ return match
71
+ end
72
+
73
+ def bodies_match?(input_body, match_body)
74
+ match = false
75
+ if (input_body.eql? match_body)
76
+ match = true
77
+ elsif (input_body.to_s.gsub(/\s+/, "").hash.eql?(match_body.to_s.gsub(/\s+/, "").hash))
78
+ match = true
79
+ end
80
+ return match
81
+ end
82
+
83
+ def pop_first_input(matches)
84
+ result = matches.first
85
+ self.delete_by_input(result)
86
+ return result
87
+ end
88
+ end
89
+
90
+ end
@@ -0,0 +1,42 @@
1
+ require 'time'
2
+ require 'json'
3
+
4
+ module Mocktopus
5
+
6
+ class MockApiCall
7
+
8
+ attr_accessor :timestamp,
9
+ :path,
10
+ :verb,
11
+ :headers,
12
+ :body
13
+
14
+ def initialize(path, verb, headers, body)
15
+ @timestamp = Time.now.utc.iso8601(10)
16
+ @path = path
17
+ @verb = verb
18
+ @headers = headers
19
+ begin
20
+ @body = JSON.parse(body)
21
+ rescue
22
+ @body = body
23
+ end
24
+ end
25
+
26
+ def to_hash
27
+ {
28
+ 'timestamp' => @timestamp,
29
+ 'path' => @path,
30
+ 'verb' => @verb,
31
+ 'headers' => @headers,
32
+ 'body' => @body
33
+ }
34
+ end
35
+
36
+ def to_s
37
+ self.to_hash.to_json
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,20 @@
1
+ module Mocktopus
2
+
3
+ class MockApiCallContainer
4
+ def initialize
5
+ @calls = []
6
+ end
7
+
8
+ def add(mock_api_call)
9
+ @calls << mock_api_call
10
+ end
11
+
12
+ def all
13
+ @calls.collect(&:to_hash)
14
+ end
15
+
16
+ def delete_all
17
+ @calls = []
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ require 'json'
2
+
3
+ module Mocktopus
4
+
5
+ class Response
6
+
7
+ class Error < Exception
8
+ end
9
+
10
+ class ValidationError < Error
11
+ end
12
+
13
+ attr_accessor :code,
14
+ :headers,
15
+ :body,
16
+ :delay
17
+
18
+ def initialize(hash)
19
+ LOGGER.debug("initializing response object")
20
+ @code = hash['code']
21
+ @headers = hash['headers']
22
+ @body = hash['body']
23
+ @delay = hash['delay'].to_f || 0
24
+
25
+ begin
26
+ @body = JSON.pretty_generate(@body)
27
+ rescue JSON::GeneratorError
28
+ @body = @body.to_s
29
+ end
30
+
31
+ validate_instance_variables()
32
+
33
+ LOGGER.debug("initialized response object from hash #{hash.inspect()}")
34
+ end
35
+
36
+ def to_hash
37
+ {
38
+ 'code' => @code,
39
+ 'headers' => @headers,
40
+ 'body' => @body,
41
+ 'delay' => @delay
42
+ }
43
+ end
44
+
45
+ private
46
+ def validate_instance_variables
47
+
48
+ if(@code.nil? || !(@code.to_i.between?(100,599) ))
49
+ raise ValidationError, "\"#{@code}\" is not a valid return code."
50
+ end
51
+
52
+ unless @headers.nil?
53
+ @headers.each do |key, value|
54
+ unless String.eql? key.class and String.eql? value.class
55
+ raise ValidationError, "\"#{key}\" => \"#{value}\" is not a valid header"
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
64
+ end