mocktopus 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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