flipp-mockserver-client 0.1.0

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.
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