flipp-mockserver-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rubocop.yml +7 -0
  4. data/Gemfile +4 -0
  5. data/README.md +182 -0
  6. data/Rakefile +10 -0
  7. data/bin/mockserver +9 -0
  8. data/lib/cli.rb +146 -0
  9. data/lib/mockserver-client.rb +17 -0
  10. data/lib/mockserver/abstract_client.rb +111 -0
  11. data/lib/mockserver/mock_server_client.rb +59 -0
  12. data/lib/mockserver/model/array_of.rb +85 -0
  13. data/lib/mockserver/model/body.rb +56 -0
  14. data/lib/mockserver/model/cookie.rb +36 -0
  15. data/lib/mockserver/model/delay.rb +34 -0
  16. data/lib/mockserver/model/enum.rb +47 -0
  17. data/lib/mockserver/model/expectation.rb +158 -0
  18. data/lib/mockserver/model/forward.rb +41 -0
  19. data/lib/mockserver/model/header.rb +43 -0
  20. data/lib/mockserver/model/parameter.rb +43 -0
  21. data/lib/mockserver/model/request.rb +77 -0
  22. data/lib/mockserver/model/response.rb +45 -0
  23. data/lib/mockserver/model/times.rb +61 -0
  24. data/lib/mockserver/proxy_client.rb +9 -0
  25. data/lib/mockserver/utility_methods.rb +59 -0
  26. data/lib/mockserver/version.rb +5 -0
  27. data/mockserver-client.gemspec +36 -0
  28. data/pom.xml +116 -0
  29. data/spec/fixtures/forward_mockserver.json +7 -0
  30. data/spec/fixtures/incorrect_login_response.json +20 -0
  31. data/spec/fixtures/post_login_request.json +22 -0
  32. data/spec/fixtures/register_expectation.json +50 -0
  33. data/spec/fixtures/retrieved_request.json +22 -0
  34. data/spec/fixtures/search_request.json +6 -0
  35. data/spec/fixtures/times_once.json +6 -0
  36. data/spec/integration/mock_client_integration_spec.rb +82 -0
  37. data/spec/mockserver/builder_spec.rb +136 -0
  38. data/spec/mockserver/mock_client_spec.rb +80 -0
  39. data/spec/mockserver/proxy_client_spec.rb +38 -0
  40. data/spec/spec_helper.rb +61 -0
  41. metadata +293 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c0442883161829df4b30bd7e65d12003a75e381d
4
+ data.tar.gz: 98dd73428d57662b761c52b7894ec76154577e76
5
+ SHA512:
6
+ metadata.gz: d2b506eb6d651fa03636ef9cee170062dcd2d87af8746f273314e6a485d060253341973fb7d4aa956cf2b61cf186d9674b760996a49a6d1f4e8696bd438cb8e3
7
+ data.tar.gz: 227e278980bbb3c75c0cd42e6bfff4630566bea4c2e4c21d3146d96570aabfb952efd27fcc1f3a901e85e0315a485c0b15b5fddf7c7e54bc9cccab64d7cd4b30
@@ -0,0 +1,21 @@
1
+ # Ruby #
2
+ ########
3
+ *.gem
4
+ *.rbc
5
+ *.bundle
6
+ *.so
7
+ *.o
8
+ *.a
9
+ *.log
10
+ .config
11
+ .yardoc
12
+ .rakeTasks
13
+ Gemfile.lock
14
+ InstalledFiles
15
+ coverage
16
+ *doc
17
+ *tmp
18
+ lib/bundler/man
19
+ pkg
20
+ spec/reports
21
+ vendor/bundle
@@ -0,0 +1,7 @@
1
+ Style/LineLength:
2
+ Max: 180
3
+ Style/ClassAndModuleChildren:
4
+ EnforcedStyle: compact
5
+ Style/FileName:
6
+ Exclude:
7
+ - '**/mockserver-client.rb'
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Gem's dependencies in mockserver-client.gemspec
4
+ gemspec
@@ -0,0 +1,182 @@
1
+ # Flipp - Modified Mockserver Client
2
+
3
+ A Ruby client for [MockServer](http://www.mock-server.com) project. This client follows the Java client's fluent style closely by using Ruby blocks.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mockserver-client'
11
+ ```
12
+
13
+ Create and install the gem:
14
+
15
+ ```bash
16
+ gem build [gem_name].gemspec
17
+ gem install [gem_name-version].gem
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ The Ruby code here assumes you have included `MockServer` and `MockServer::Model::DSL` modules. So these lines should typically be at the top of your code:
23
+
24
+ ```ruby
25
+ include MockServer
26
+ include MockServer::Model::DSL
27
+ ```
28
+
29
+ ### Create Expectations
30
+
31
+ ##### Ruby
32
+
33
+ ```ruby
34
+ client = MockServerClient.new('localhost', 9999)
35
+ expectation = expectation do |expectation|
36
+ expectation.request do |request|
37
+ request.method = 'POST'
38
+ request.path = '/login'
39
+ request.query_string_parameters << parameter('returnUrl', '/account')
40
+ request.cookies = [cookie('sessionId', '2By8LOhBmaW5nZXJwcmludCIlMDAzMW')]
41
+ request.body = exact("{username: 'foo', password: 'bar'}")
42
+ end
43
+
44
+ expectation.response do |response|
45
+ response.status_code = 401
46
+ response.headers << header('Content-Type', 'application/json; charset=utf-8')
47
+ response.headers << header('Cache-Control', 'public, max-age=86400')
48
+ response.body = body("{ message: 'incorrect username and password combination' }")
49
+ response.delay = delay_by(:SECONDS, 1)
50
+ end
51
+ end
52
+ client.register(expectation)
53
+ ```
54
+
55
+ ### Clear & Reset Server
56
+
57
+ ##### Ruby
58
+
59
+ ```ruby
60
+ client.clear(request('POST', '/login'))
61
+ client.reset
62
+ ```
63
+ ### Verifying Behavior
64
+
65
+ ##### Ruby
66
+
67
+ ```ruby
68
+ # Not providing times here because the default is exactly(1) i.e. the second argument to verify method
69
+ ProxyClient.new('localhost', 9090).verify(request(:POST, '/login') do |request|
70
+ request.body = exact("{username: 'foo', password: 'bar'}")
71
+ request.cookies = [cookie("sessionId", ".*")]
72
+ end)
73
+ ```
74
+
75
+
76
+ ### Analyzing Behavior
77
+
78
+ ##### Ruby
79
+
80
+ ```ruby
81
+ # Second argument is true to set output to Java; false to set output to default JSON (default)
82
+ ProxyClient.new('localhost', 9090).dump_log(request(:POST, '/login'), false)
83
+ ```
84
+
85
+ ## Complete Ruby DSL
86
+ The DSL is provided via the `MockServer::Model::DSL` module. Include this module in your code to make the DSL available.
87
+
88
+ Request
89
+ * **request**: Used to build a request object. If a block is passed, will configure request first and then return the configured request. Example: `request(:POST, '/login') {|r| r.headers << header("Content-Type", "application/json")}`.
90
+ * **http_request**: Alias for `request`.
91
+
92
+ Body
93
+ * **body**: Create a string body object (use in a response). Example: `body("unaccepted")`.
94
+ * **exact**: Create a body of type `STRING`. Example: `exact('{"reason": "unauthorized"}')`.
95
+ * **regex**: Create a body of type `REGEX`. Example: `regex('username[a-z]{4}')`.
96
+ * **xpath**: Used to create a body of type `XPATH`. Example: `xpath("/element[key = 'some_key' and value = 'some_value']")`.
97
+ * **parameterized**; Create a body to type `PARAMETERS`. Example `parameterized(parameter('someValue', 1, 2), parameter('otherValue', 4, 5))`.
98
+
99
+ Parameters
100
+ * **parameter**: Create a generic parameter. Example: `parameter('key', 'value1' , 'value2')`.
101
+ * **cookie**: Create a cookie (same as `parameter` above but exists as syntactic sugar). Example: `cookie('sessionId', 'sessionid1ldj')`.
102
+ * **header**: Create a header (same as `parameter` above but exists as syntactic sugar). Example: `header('Content-Type', 'application/json')`.
103
+
104
+ Forward
105
+ * **forward**: Create a forwarding response. If a block is passed, will configure forward response first and then return the configured object. Example: `forward {|f| f.scheme = 'HTTPS' }`.
106
+ * **http_forward**: Alias for `forward`.
107
+
108
+ Response
109
+ * **response**: Create a response object. If a block is passed, will configure response first and then return the configured response. Example: `response {|r| r.status_code = 201 }`.
110
+ * **http_response**: Alias for `response`.
111
+
112
+ Delay
113
+ * **delay_by**. Create a delay object in a response. Example : `delay_by(:MICROSECONDS, 20)`.
114
+
115
+ Times (used in Expectation)
116
+ * **times**: Create an 'times' object. If a block is passed, will configure the object first before returning it. Example: `times {|t| t.unlimited = false }`.
117
+ * **unlimited**: Create an object with unlimited repeats. Example: `unlimited()`. (No parameters).
118
+ * **once**. Create an object that repeats only once. Example: `once()`. (No parameters).
119
+ * **exactly**. Create an object that repeats exactly the number of times specified. Example: `exactly(2)`.
120
+ * **at_least**: Create an object that repeats at least the given number of times. (Use in verify). Example: `at_least(2)`.
121
+
122
+ Expectation (use in register)
123
+ * **expectation**: Create an expectation object. If a block is passed, will configure the object first before returning it. Example: `expectation {|e| e.request {|r| r.path = "index.html} }`.
124
+ Getter methods for `request`, `response` and `forward` methods will optionally accept a block. If block is passed object is configured before it is returned. The attribute `times` has conventional getter and setter methods.
125
+
126
+ ## CLI
127
+
128
+ This gem comes with a command line interface which allow you to run the Mock Server calls from the command line. When this gem is installed, you will have the `mockserver` executable on your PATH. Type `mockserver --help` and you will get this output:
129
+
130
+ ```bash
131
+ mockserver --help
132
+
133
+ Commands:
134
+ mockserver clear # Clears all stored mock request/responses from server.
135
+ mockserver dump_log # Dumps the matching request to the mock server logs.
136
+ mockserver help [COMMAND] # Describe available commands or one specific command
137
+ mockserver register # Register an expectation with the mock server.
138
+ mockserver reset # Resets the server clearing all data.
139
+ mockserver retrieve # Retrieve the list of requests that have been made to the mock/proxy server.
140
+ mockserver verify # Verify that a request has been made the specified number of times to the server.
141
+
142
+ Options:
143
+ -h, --host=HOST # The host for the MockServer client.
144
+ # Default: localhost
145
+ -p, --port=N # The port for the MockServer client.
146
+ # Default: 8080
147
+ -d, [--data=DATA] # A JSON or YAML file containing the request payload.
148
+ ```
149
+
150
+ To get help for an individual command, e.g. `dump_log`, you would do:
151
+
152
+ ```bash
153
+ mockserver --help dump_log
154
+
155
+ Usage:
156
+ mockserver dump_log
157
+
158
+ Options:
159
+ -j, [--java], [--no-java] # A switch to turn Java format for logs on/off.
160
+ -h, --host=HOST # The host for the MockServer client.
161
+ # Default: localhost
162
+ -p, --port=N # The port for the MockServer client.
163
+ # Default: 8080
164
+ -d, [--data=DATA] # A JSON or YAML file containing the request payload.
165
+
166
+ Dumps the matching request to the mock server logs.
167
+ ```
168
+
169
+ Here is an example on how you would run the command:
170
+
171
+ ```bash
172
+ mockserver dump_log -j true
173
+
174
+ Running with parameters:
175
+ host: localhost
176
+ port: 8080
177
+ java: true
178
+
179
+ [2014-06-21 09:23:32] DEBUG [MockServerClient] Sending dump log request to mockserver
180
+ [2014-06-21 09:23:32] DEBUG [MockServerClient] URL: /dumpToLog?type=java. Payload: {}
181
+ [2014-06-21 09:23:32] DEBUG [MockServerClient] Got dump to log response: 202
182
+ ```
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+ require 'bundler/gem_tasks'
3
+ require 'rubocop/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ desc 'Main task for this project to ensure the project passes build'
10
+ task default: [:rubocop, :spec, :build]
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ #!/usr/bin/env ruby
3
+
4
+ #
5
+ # A thor-based runner for commands in this project
6
+ # @author: Nayyara Samuel (mailto: nayyara.samuel@opower.com)
7
+ #
8
+ require_relative '../lib/cli'
9
+ MockServerCLI.start
@@ -0,0 +1,146 @@
1
+ # encoding: UTF-8
2
+ require 'thor'
3
+ require 'colorize'
4
+ require_relative './mockserver-client'
5
+
6
+ # CLI for this gem
7
+ # @author Nayyara Samuel(nayyara.samuel@opower.com)
8
+ #
9
+ module CLIHelpers
10
+ include MockServer
11
+
12
+ LOGGER = LoggingFactory::DEFAULT_FACTORY.log('MockServerClient')
13
+
14
+ # Prints out the parameters passed to it
15
+ # @param options [Hash] a hash of parameters
16
+ def print_parameters(options)
17
+ puts "\nRunning with parameters:".bold
18
+ options.each { |k, v| puts "\t#{k}: #{v}".yellow }
19
+ puts ''
20
+ end
21
+
22
+ # Create a mockserver client
23
+ # @param options [Struct] with host and port set
24
+ # @return [MockServerClient] the mockserver client with the host and port
25
+ def mockserver_client(options)
26
+ client = MockServerClient.new(options.host, options.port)
27
+ client.logger = LOGGER
28
+ client
29
+ end
30
+
31
+ # Create a proxy client
32
+ # @param options [Struct] with host and port set
33
+ # @return [ProxyClient] the proxy client with the host and port
34
+ def proxy_client(options)
35
+ client = ProxyClient.new(options.host, options.port)
36
+ client.logger = LOGGER
37
+ client
38
+ end
39
+
40
+ # Convert a hash to a struct
41
+ # @param hash [Hash] a hash
42
+ # @return [Struct] a struct constructed from the hash
43
+ def to_struct(hash)
44
+ hash = symbolize_keys(hash)
45
+ Struct.new(*hash.keys).new(*hash.values)
46
+ end
47
+
48
+ # Process a block using options extracted into a struct
49
+ # @param mockserver [Boolean] true to use mockserver, false to use proxy
50
+ # @yieldparam [AbstractClient] a mockserver or a proxy client
51
+ # @yieldparam [Struct] a struct created from options hash
52
+ def execute_command(mockserver = false, data_required = false, error_msg = '--data option must be provided', &_)
53
+ print_parameters(options)
54
+ struct_options = to_struct({ data: nil }.merge(options))
55
+ if data_required && !options['data']
56
+ error(error_msg)
57
+ else
58
+ client = mockserver ? mockserver_client(struct_options) : proxy_client(struct_options)
59
+ yield client, struct_options if block_given?
60
+ end
61
+ end
62
+
63
+ # Prints an error message
64
+ # @param message [String] an error message
65
+ def error(message)
66
+ puts message.red
67
+ end
68
+
69
+ # Read a file
70
+ # @param file [String] a file to read
71
+ def read_file(file)
72
+ YAML.load_file(file)
73
+ end
74
+ end
75
+
76
+ # CLI for mock server and proxy clients
77
+ class MockServerCLI < Thor
78
+ include CLIHelpers
79
+ include MockServer::UtilityMethods
80
+ include MockServer::Model::DSL
81
+
82
+ class_option :host, type: :string, aliases: '-h', required: true, default: 'localhost', desc: 'The host for the MockServer client.'
83
+ class_option :port, type: :numeric, aliases: '-p', required: true, default: 8080, desc: 'The port for the MockServer client.'
84
+ class_option :data, type: :string, aliases: '-d', desc: 'A JSON or YAML file containing the request payload.'
85
+
86
+ desc 'retrieve', 'Retrieve the list of requests that have been made to the mock/proxy server.'
87
+
88
+ def retrieve
89
+ execute_command do |client, _|
90
+ result = options.data ? client.retrieve(read_file(options.data)) : client.retrieve
91
+ puts "RESULT:\n".bold + "#{result.to_json}".green
92
+ end
93
+ end
94
+
95
+ desc 'register', 'Register an expectation with the mock server.'
96
+
97
+ def register
98
+ execute_command(true, true) do |client, options|
99
+ payload = read_file(options.data)
100
+ mock_expectation = expectation do |expectation|
101
+ expectation.populate_from_payload(payload)
102
+ end
103
+ client.register(mock_expectation)
104
+ end
105
+ end
106
+
107
+ desc 'dump_log', 'Dumps the matching request to the mock server logs.'
108
+ option :java, type: :boolean, aliases: '-j', default: false, desc: 'A switch to turn Java format for logs on/off.'
109
+
110
+ def dump_log
111
+ execute_command do |client, options|
112
+ options.data ? client.dump_log(read_file(options.data), options.java) : client.dump_log(nil, options.java)
113
+ end
114
+ end
115
+
116
+ desc 'clear', 'Clears all stored mock request/responses from server.'
117
+
118
+ def clear
119
+ error_message = 'ERROR: No request provided. HINT: Use `clear` to selectively clear requests. Use `reset` to clear all.'
120
+ execute_command(false, true, error_message) do |client, _|
121
+ payload = read_file(options.data)
122
+ client.clear(payload)
123
+ end
124
+ end
125
+
126
+ desc 'reset', 'Resets the server clearing all data.'
127
+
128
+ def reset
129
+ execute_command do |client, _|
130
+ client.reset
131
+ end
132
+ end
133
+
134
+ desc 'verify', 'Verify that a request has been made the specified number of times to the server.'
135
+
136
+ def verify
137
+ execute_command(false, true) do |client, _|
138
+ payload = read_file(options.data)
139
+ mock_request = payload[HTTP_REQUEST]
140
+ mock_times = payload[HTTP_TIMES]
141
+
142
+ error 'No request found for verifying against' unless mock_request
143
+ mock_times ? client.verify(mock_request, mock_times) : client.verify(mock_request)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+ require_relative './mockserver/version'
3
+ require_relative './mockserver/mock_server_client'
4
+ require_relative './mockserver/proxy_client'
5
+
6
+ # Setup serialization correctly with multi_json
7
+ require 'json/pure'
8
+
9
+ # To fix serialization bugs. See: http://prettystatemachine.blogspot.com/2010/09/typeerrors-in-tojson-make-me-briefly.html
10
+ class Fixnum
11
+ def to_json(_)
12
+ to_s
13
+ end
14
+ end
15
+
16
+ require 'multi_json'
17
+ MultiJson.use(:json_pure)
@@ -0,0 +1,111 @@
1
+ # encoding: UTF-8
2
+ require 'rest-client'
3
+ require 'logging_factory'
4
+ require_relative './model/times'
5
+ require_relative './model/request'
6
+ require_relative './model/expectation'
7
+ require_relative './utility_methods'
8
+
9
+ # An abstract client for making requests supported by mock and proxy client.
10
+ # @author Nayyara Samuel(mailto: nayyara.samuel@opower.com)
11
+ #
12
+ module MockServer
13
+ RESET_ENDPOINT = '/reset'
14
+ CLEAR_ENDPOINT = '/clear'
15
+ RETRIEVE_ENDPOINT = '/retrieve'
16
+ DUMP_LOG_ENDPOINT = '/dumpToLog'
17
+
18
+ # An abstract client for making requests supported by mock and proxy client.
19
+ class AbstractClient
20
+ include Model::DSL
21
+ include Model
22
+ include UtilityMethods
23
+
24
+ attr_accessor :logger
25
+
26
+ def initialize(host, port)
27
+ fail 'Cannot instantiate AbstractClient class. You must subclass it.' if self.class == AbstractClient
28
+ fail 'Host/port must not be nil' unless host && port
29
+ @base = RestClient::Resource.new("http://#{host}:#{port}", headers: { 'Content-Type' => 'application/json' })
30
+ @logger = ::LoggingFactory::DEFAULT_FACTORY.log(self.class)
31
+ end
32
+
33
+ # Clear all expectations with the given request
34
+ # @param request [Request] the request to use to clear an expectation
35
+ # @return [Object] the response from the clear action
36
+ def clear(request)
37
+ request = camelized_hash(HTTP_REQUEST => Request.new(symbolize_keys(request)))
38
+
39
+ logger.debug("Clearing expectation with request: #{request}")
40
+ logger.debug("URL: #{CLEAR_ENDPOINT}. Payload: #{request.to_hash}")
41
+
42
+ response = @base[CLEAR_ENDPOINT].put(request.to_json, content_type: :json)
43
+ logger.debug("Got clear response: #{response.code}")
44
+ parse_string_to_json(response)
45
+ end
46
+
47
+ # Reset the mock server clearing all expectations previously registered
48
+ # @return [Object] the response from the reset action
49
+ def reset
50
+ request = {}
51
+
52
+ logger.debug('Resetting mockserver')
53
+ logger.debug("URL: #{RESET_ENDPOINT}. Payload: #{request.to_hash}")
54
+
55
+ response = @base[RESET_ENDPOINT].put(request.to_json)
56
+ logger.debug("Got reset response: #{response.code}")
57
+ parse_string_to_json(response)
58
+ end
59
+
60
+ # Retrieve the list of requests that have been processed by the server
61
+ # @param request [Request] to filter requests
62
+ # @return [Object] the list of responses processed by the server
63
+ def retrieve(request = nil)
64
+ request = request ? camelized_hash(HTTP_REQUEST => Request.new(symbolize_keys(request))) : {}
65
+
66
+ logger.debug('Retrieving request list from mockserver')
67
+ logger.debug("URL: #{RETRIEVE_ENDPOINT}. Payload: #{request.to_hash}")
68
+
69
+ response = @base[RETRIEVE_ENDPOINT].put(request.to_json)
70
+ logger.debug("Got retrieve response: #{response.code}")
71
+ requests = Requests.new([])
72
+ parse_string_to_json(response.body).map { |result| requests << request_from_json(result) } unless response.empty?
73
+ requests.code = response.code
74
+ requests
75
+ end
76
+
77
+ # Request to dump logs to file
78
+ # @param java [Boolean] true to dump as Java code; false to dump as JSON
79
+ # @return [Object] the list of responses processed by the server
80
+ def dump_log(request = nil, java = false)
81
+ type_params = java ? '?type=java' : ''
82
+ url = "#{DUMP_LOG_ENDPOINT}#{type_params}"
83
+ request = request ? Request.new(symbolize_keys(request)) : {}
84
+
85
+ logger.debug('Sending dump log request to mockserver')
86
+ logger.debug("URL: #{url}. Payload: #{request.to_hash}")
87
+
88
+ response = @base[url].put(request.to_json)
89
+ logger.debug("Got dump to log response: #{response.code}")
90
+ parse_string_to_json(response)
91
+ end
92
+
93
+ # Verify that the given request is called the number of times expected
94
+ # @param request [Request] to filter requests
95
+ # @param times [Times] expected number of times
96
+ # @return [Object] the list of responses processed by the server that match the request
97
+ def verify(request, times = exactly(1))
98
+ logger.debug('Sending query for verify to mockserver')
99
+ results = retrieve(request)
100
+
101
+ # Reusing the times model here so interpreting values here
102
+ times = Times.new(symbolize_keys(times))
103
+ num_times = times.remaining_times
104
+ is_exact = !times.unlimited
105
+
106
+ fulfilled = is_exact ? (num_times == results.size) : (num_times <= results.size)
107
+ fail "Expected request to be present: [#{num_times}] (#{is_exact ? 'exactly' : 'at least'}). But found: [#{results.size}]" unless fulfilled
108
+ results
109
+ end
110
+ end
111
+ end