tshield 0.10.0.0 → 0.11.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64cf908b05ea57f6314d850f0474f11e830d561f1e91db2c6b8cb9edb4fbf786
4
- data.tar.gz: f2e16b6b56318a5e4cd710b24720aa87439d280a1e17bc1bacada981bc89eaff
3
+ metadata.gz: 618a5c8f2527911612734f7aaddd21fab7ba82066409a451ad4bd39ac42eae33
4
+ data.tar.gz: 4637697bcef288f4276e664adb653eaa7c6aae7b898cdf8b238ac27a8499b55c
5
5
  SHA512:
6
- metadata.gz: 2d877bc07758ce22abfe127e6ea372343913b5d7f7529ad2989d9a4824901f5bbdb4036fc362af0a90142e7e83b6d1e2553e0a43d1f229b0ae1aa920f76d912c
7
- data.tar.gz: fb07bdc34d9b557d2e11a071b9e0825e3bc84712240cf962643fe0fdbb687e3b7eb1abd0cec991adb7863a4dd43bc49120fa7cd5d9871033b767164b814e2f96
6
+ metadata.gz: a8b7d0731557762fa096a583845865817848b301b7f82c3e6ea467953d587b116ac72dce5930698051cdaf192c6ae3a1b34d501d1bb35bc32a36b265a039017b
7
+ data.tar.gz: 440c90f16f7ef256078a115b2f725bf94ac747375a40b01b3d33d6dab8da427a598555487e70c2568e7e6b5e0585f4b9ce6e0cf0b5e3772effeacc6ce8969620
data/README.md CHANGED
@@ -25,7 +25,11 @@ To run server execute this command
25
25
  tshield
26
26
 
27
27
  Default port is **4567**
28
-
28
+
29
+ #### Command Line Options
30
+
31
+ * __-port__: Overwrite default port (4567)
32
+ * __-help__: Show all command line options
29
33
 
30
34
  #### Config example
31
35
 
@@ -49,7 +53,102 @@ domains:
49
53
  - /users
50
54
  ```
51
55
 
52
- ## Config options
56
+ ## Config options for Pattern Matching
57
+
58
+ An example of file to create a stub:
59
+
60
+ All files should be in `matching` directory.
61
+ Each file should be a valid JSON array of objects and each object must contain
62
+ at least the following attributes:
63
+
64
+ * __method__: a http method.
65
+ * __path__: url path.
66
+ * __response__: object with response data.
67
+
68
+ Response must be contain the following attributes:
69
+
70
+ * __headers__: key value object with expected headers to match. In the evaluation process
71
+ this stub will be returned if all headers are in request.
72
+ * __status__: integer used http status respose.
73
+ * __body__: content to be returned.
74
+
75
+ Optional request attributes:
76
+
77
+ * __headers__: key value object with expected headers to match. In the evaluation process
78
+ this stub will be returned if all headers are in request.
79
+ * __query__: works like headers but use query params.
80
+
81
+ __Important__: If VCR config conflicts with Matching config Matching will be
82
+ used. Matching config have priority.
83
+
84
+ ### Session Configuration
85
+
86
+ To register stub into a session create an object with following attributes:
87
+
88
+ * __session__: name of session.
89
+ * __stubs__: an array with objects described above.
90
+
91
+ ### Example of matching configuration
92
+
93
+ ```json
94
+ [{
95
+ "method": "GET",
96
+ "path": "/matching/example",
97
+ "query": {
98
+ "user": 123
99
+ },
100
+ "response": {
101
+ "body": "matching-example-response-with-query",
102
+ "headers": {},
103
+ "status": 200
104
+ }
105
+ },
106
+ {
107
+ "method": "GET",
108
+ "path": "/matching/example",
109
+ "response": {
110
+ "body": "matching-example-response",
111
+ "headers": {},
112
+ "status": 200
113
+ }
114
+ },
115
+ {
116
+ "method": "POST",
117
+ "path": "/matching/example",
118
+ "headers": {
119
+ "user": "123"
120
+ },
121
+ "response": {
122
+ "body": "matching-example-response-with-headers",
123
+ "headers": {},
124
+ "status": 200
125
+ }
126
+ },
127
+ {
128
+ "method": "POST",
129
+ "path": "/matching/example",
130
+ "response": {
131
+ "body": "matching-example-response-with-post",
132
+ "headers": {},
133
+ "status": 200
134
+ }
135
+ },
136
+ {
137
+ "session": "example-session",
138
+ "stubs": [{
139
+ "method": "GET",
140
+ "path": "/matching/example",
141
+ "response": {
142
+ "body": "matching-example-response-in-session",
143
+ "headers": {},
144
+ "status": 200
145
+ }
146
+ }]
147
+ }
148
+ ]
149
+ ```
150
+
151
+ ## Config options for VCR
53
152
  ```yaml
54
153
  request:
55
154
  timeout: 8
@@ -167,37 +266,6 @@ This features files are used as base for the component tests.
167
266
  ## Samples
168
267
  #### Basic sample for a client app requesting a server API
169
268
  [examples/client-api-nodejs](examples/client-api-nodejs)
170
- #### Basic sample for componente/integration test
171
- **[WIP]**
172
-
173
- ## Setup for local development
174
-
175
- First install dependencies.
176
- _We recommend use of the RVM to manage project dependencies.__
177
-
178
- ```
179
- bundle install
180
- ```
181
-
182
- ### Run server to development
183
-
184
- To start server execute:
185
-
186
- `rake server`
187
-
188
- ### Build
189
-
190
- To generate ruby gem execute:
191
-
192
- `rake build`
193
-
194
- ### Test
195
-
196
- To run all unit tests:
197
-
198
- `rake spec`
199
-
200
- To run all component tests:
201
-
202
- `rake component__tests`
203
269
 
270
+ ## Contributing
271
+ [Hacking or Contributing to Tshield](CONTRIBUTING.md)
data/config/tshield.yml CHANGED
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  request:
3
- timeout: 8
4
-
3
+ timeout: 10
5
4
  domains:
6
- 'https://service.com':
7
- name: 'service'
5
+ 'http://localhost:9090':
6
+ name: 'components'
7
+ skip_query_params:
8
+ - b
8
9
  paths:
9
10
  - /users
11
+ - /resources
data/lib/tshield.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tshield/extensions/string_extensions'
3
4
  require 'tshield/options'
4
5
  require 'tshield/simple_tcp_server'
5
6
  require 'tshield/server'
@@ -25,6 +25,8 @@ module TShield
25
25
  # (NEED IMPROVEMENT github-issue #https://github.com/diegorubin/tshield/issues/17)
26
26
  # not_save_headers: List of headers that should be ignored in generated
27
27
  # file
28
+ # ignore_query_params: List of params that should be ignored in
29
+ # generated directory
28
30
  #
29
31
  attr_reader :request
30
32
  attr_reader :domains
@@ -63,7 +65,7 @@ module TShield
63
65
  end
64
66
 
65
67
  def get_headers(domain)
66
- domains[domain]['headers'] || {}
68
+ (domains[domain] || {})['headers'] || {}
67
69
  end
68
70
 
69
71
  def get_name(domain)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TShield
4
+ module Controllers
5
+ module Helpers
6
+ # Session Helpers
7
+ module SessionHelpers
8
+ def self.current_session_name(request)
9
+ session = TShield::Sessions.current(request.ip)
10
+ session ? session[:name] : 'no-session'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,15 +2,16 @@
2
2
 
3
3
  require 'sinatra'
4
4
 
5
- require 'byebug'
6
-
5
+ require 'tshield/controllers/helpers/session_helpers'
7
6
  require 'tshield/options'
8
7
  require 'tshield/configuration'
9
- require 'tshield/request'
8
+ require 'tshield/request_matching'
9
+ require 'tshield/request_vcr'
10
10
  require 'tshield/sessions'
11
11
 
12
12
  module TShield
13
13
  module Controllers
14
+ # Requests Handler
14
15
  module Requests
15
16
  PATHP = %r{([a-zA-Z0-9/\._-]+)}.freeze
16
17
 
@@ -44,25 +45,26 @@ module TShield
44
45
  end
45
46
  end
46
47
 
48
+ # Requests Handler Helpers
47
49
  module Helpers
50
+ def self.build_headers(request)
51
+ headers = request.env.select { |key, _value| key =~ /HTTP/ }
52
+ headers['Content-Type'] = request.content_type || 'application/json'
53
+ headers
54
+ end
55
+
48
56
  def treat(params, request, _response)
49
57
  path = params.fetch('captures', [])[0]
50
58
 
51
- debugger if TShield::Options.instance.break?(path: path, moment: :before)
52
-
53
59
  method = request.request_method
54
60
  request_content_type = request.content_type
55
-
56
- headers = {
57
- 'Content-Type' => request.content_type || 'application/json'
58
- }
59
-
60
- add_headers(headers, path)
61
+ session_name = TShield::Controllers::Helpers::SessionHelpers.current_session_name(request)
61
62
 
62
63
  options = {
63
64
  method: method,
64
- headers: headers,
65
+ headers: Helpers.build_headers(request),
65
66
  raw_query: request.env['QUERY_STRING'],
67
+ session: session_name,
66
68
  ip: request.ip
67
69
  }
68
70
 
@@ -73,13 +75,18 @@ module TShield
73
75
  replace: '')
74
76
  options[:body] = result
75
77
  end
78
+ api_response = TShield::RequestMatching.new(path, options).match_request
76
79
 
77
- set_content_type content_type
80
+ unless api_response
81
+ add_headers(headers, path)
78
82
 
79
- api_response = TShield::Request.new(path, options).response
83
+ api_response ||= TShield::RequestVCR.new(path, options).response
84
+ end
80
85
 
81
86
  logger.info(
82
- "original=#{api_response.original} method=#{method} path=#{path} content-type=#{request_content_type} session=#{current_session_name(request)}"
87
+ "original=#{api_response.original} method=#{method} path=#{path}"\
88
+ "content-type=#{request_content_type}"\
89
+ "session=#{session_name}"
83
90
  )
84
91
 
85
92
  status api_response.status
@@ -87,17 +94,8 @@ module TShield
87
94
  body api_response.body
88
95
  end
89
96
 
90
- def set_content_type(_request_content_type)
91
- content_type :json
92
- end
93
-
94
- def current_session_name(request)
95
- session = TShield::Sessions.current(request.ip)
96
- session ? session[:name] : 'no-session'
97
- end
98
-
99
97
  def add_headers(headers, path)
100
- configuration.get_headers(domain(path)).each do |source, destiny|
98
+ (configuration.get_headers(domain(path)) || {}).each do |source, destiny|
101
99
  headers[destiny] = request.env[source] unless request.env[source].nil?
102
100
  end
103
101
  end
@@ -7,18 +7,31 @@ require 'tshield/sessions'
7
7
 
8
8
  module TShield
9
9
  module Controllers
10
+ # Actions to handle sessions
10
11
  module Sessions
11
12
  def self.registered(app)
12
- app.get TShield::Configuration.singleton.session_path do
13
- TShield::Sessions.current(request.ip).to_json
13
+ session_path = TShield::Configuration.singleton.session_path
14
+ ip = request.ip
15
+ register_get(app, session_path, ip)
16
+ register_post(app, session_path, ip, params)
17
+ register_delete(app, session_path, ip)
18
+ end
19
+
20
+ def self.register_get(app, session_path, ip)
21
+ app.get session_path do
22
+ TShield::Sessions.current(ip).to_json
14
23
  end
24
+ end
15
25
 
16
- app.post TShield::Configuration.singleton.session_path do
17
- TShield::Sessions.start(request.ip, params[:name]).to_json
26
+ def self.register_post(app, session_path, params, ip)
27
+ app.post session_path do
28
+ TShield::Sessions.start(ip, params[:name]).to_json
18
29
  end
30
+ end
19
31
 
20
- app.delete TShield::Configuration.singleton.session_path do
21
- TShield::Sessions.stop(request.ip).to_json
32
+ def self.register_delete(app, session_path, ip)
33
+ app.delete session_path do
34
+ TShield::Sessions.stop(ip).to_json
22
35
  end
23
36
  end
24
37
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # String Extensions
4
+ module StringExtensions
5
+ def to_rack_name
6
+ "HTTP_#{upcase.tr('-', '_')}"
7
+ end
8
+ end
9
+
10
+ String.include StringExtensions
@@ -23,35 +23,20 @@ module TShield
23
23
  parse unless options[:skip_parse]
24
24
  end
25
25
 
26
- def break?(args = {})
27
- check_breakpoint(args)
28
- end
29
-
30
26
  def configuration_file
31
27
  @options.fetch(:configuration_file, 'config/tshield.yml')
32
28
  end
33
29
 
34
- private
35
-
36
- def check_breakpoint(args)
37
- check_breakpoint_moment(args)
38
- end
39
-
40
- def check_breakpoint_moment(args)
41
- @options["#{args[:moment]}_pattern".to_sym] =~ args[:path]
30
+ def port
31
+ @options.fetch(:port, 4567)
42
32
  end
43
33
 
44
- def register_before_pattern(opts)
45
- opts.on('-b', '--break-before-request [PATTERN]',
46
- 'Breakpoint before request') do |pattern|
47
- @options[:before_pattern] = Regexp.new(pattern)
48
- end
49
- end
34
+ private
50
35
 
51
- def register_after_pattern(opts)
52
- opts.on('-a', '--break-after-request [PATTERN]',
53
- 'Breakpoint after request') do |pattern|
54
- @options[:after_pattern] = Regexp.new(pattern)
36
+ def register_port(opts)
37
+ opts.on('-p', '--port [PORT]',
38
+ 'Sinatra port') do |port|
39
+ @options[:port] = port.to_i
55
40
  end
56
41
  end
57
42
 
@@ -62,11 +47,6 @@ module TShield
62
47
  end
63
48
  end
64
49
 
65
- def register_patterns(opts)
66
- register_before_pattern(opts)
67
- register_after_pattern(opts)
68
- end
69
-
70
50
  def register_version(opts)
71
51
  opts.on('-v', '--version', 'Show version') do
72
52
  TShield.logger.info(TShield::Version.to_s)
@@ -90,8 +70,8 @@ module TShield
90
70
 
91
71
  def register(opts)
92
72
  register_configuration(opts)
93
- register_patterns(opts)
94
73
  register_version(opts)
74
+ register_port(opts)
95
75
  register_help(opts)
96
76
  end
97
77
  end
@@ -1,166 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'httparty'
4
- require 'json'
5
- require 'byebug'
6
-
7
- require 'digest/sha1'
8
-
9
- require 'tshield/configuration'
10
- require 'tshield/options'
11
- require 'tshield/response'
12
3
  require 'tshield/sessions'
13
4
 
14
5
  module TShield
6
+ # Base of request mock methods
15
7
  class Request
16
- attr_reader :response
8
+ attr_reader :configuration
9
+ attr_writer :content_idx
17
10
 
18
- def initialize(path, options = {})
19
- @path = path
20
- @options = options
11
+ def initialize
21
12
  @configuration = TShield::Configuration.singleton
22
- @options[:timeout] = @configuration.request['timeout']
23
- @options[:verify] = @configuration.request['verify_ssl']
24
- request
25
- end
26
-
27
- def request
28
- unless @options[:raw_query].nil? || @options[:raw_query].empty?
29
- @path = "#{@path}?#{@options[:raw_query]}"
30
- end
31
-
32
- @url = "#{domain}#{@path}"
33
-
34
- if exists
35
- @response = get_current_response
36
- @response.original = false
37
- else
38
- @method = method
39
- @configuration.get_before_filters(domain).each do |filter|
40
- @method, @url, @options = filter.new.filter(@method, @url, @options)
41
- end
42
-
43
- raw = HTTParty.send(@method.to_s, @url, @options)
44
-
45
- @configuration.get_after_filters(domain).each do |filter|
46
- raw = filter.new.filter(raw)
47
- end
48
-
49
- @response = save(raw)
50
-
51
- @response.original = true
52
- end
53
- current_session[:counter].add(@path, method) if current_session
54
- debugger if TShield::Options.instance.break?(path: @path, moment: :after)
55
- @response
56
- end
57
-
58
- private
59
-
60
- def domain
61
- @domain ||= @configuration.get_domain_for(@path)
62
- end
63
-
64
- def name
65
- @name ||= @configuration.get_name(domain)
66
- end
67
-
68
- def method
69
- @options[:method].downcase
70
13
  end
71
14
 
72
- def save(raw_response)
73
- headers = {}
74
- raw_response.headers.each do |k, v|
75
- headers[k] = v unless @configuration.not_save_headers(domain).include? k
76
- end
77
-
78
- content = {
79
- body: raw_response.body,
80
- status: raw_response.code,
81
- headers: headers
82
- }
83
-
84
- write(content)
85
-
86
- TShield::Response.new(raw_response.body, headers, raw_response.code)
87
- end
15
+ protected
88
16
 
89
17
  def current_session
90
18
  TShield::Sessions.current(@options[:ip])
91
19
  end
92
20
 
93
- def content
94
- return @content if @content
95
-
96
- @content = JSON.parse(File.open(destiny).read)
97
- @content['body'] = File.open(destiny(true)).read unless @content['body']
98
- @content
99
- end
100
-
101
- def file_exists
21
+ def session_destiny(request_path)
102
22
  session = current_session
103
- @content_idx = session ? session[:counter].current(@path, method) : 0
104
- File.exist?(destiny)
105
- end
23
+ return unless session
106
24
 
107
- def exists
108
- file_exists && @configuration.cache_request?(domain)
25
+ request_path = File.join(request_path, session[:name])
26
+ Dir.mkdir(request_path) unless File.exist?(request_path)
109
27
  end
110
28
 
111
- def get_current_response
112
- TShield::Response.new(content['body'], content['headers'] || [], content['status'] || 200)
29
+ def content_destiny
30
+ "#{destiny}.content"
113
31
  end
114
32
 
115
- def key
116
- @key ||= Digest::SHA1.hexdigest "#{@url}|#{method}"
33
+ def headers_destiny
34
+ "#{destiny}.json"
117
35
  end
118
36
 
119
- def destiny(iscontent = false)
37
+ def destiny
120
38
  request_path = File.join('requests')
121
39
  Dir.mkdir(request_path) unless File.exist?(request_path)
122
40
 
123
- session = current_session
124
- if session
125
- request_path = File.join(request_path, session[:name])
126
- Dir.mkdir(request_path) unless File.exist?(request_path)
127
- end
41
+ session_destiny(request_path)
128
42
 
129
43
  name_path = File.join(request_path, name)
130
44
  Dir.mkdir(name_path) unless File.exist?(name_path)
131
45
 
132
- path_path = File.join(name_path, safe_dir(@path))
46
+ cleared_path = clear_path(@path)
47
+ path_path = File.join(name_path, safe_dir(cleared_path))
48
+
133
49
  Dir.mkdir(path_path) unless File.exist?(path_path)
134
50
 
135
51
  method_path = File.join(path_path, method)
136
52
  Dir.mkdir(method_path) unless File.exist?(method_path)
137
53
 
138
- destiny_name = iscontent ? "#{@content_idx}.content" : "#{@content_idx}.json"
139
- File.join(method_path, destiny_name)
54
+ File.join(method_path, @content_idx.to_s)
140
55
  end
141
56
 
142
- def write(content)
143
- f = File.open(destiny(true), 'w')
144
- f.write(content[:body])
145
- f.close
57
+ def clear_path(path)
58
+ skip_query_params = configuration.domains[@domain]['skip_query_params']
59
+ url_path, params = path.split('?')
146
60
 
147
- body = content.delete :body
61
+ return path if !skip_query_params || !params
148
62
 
149
- f = File.open(destiny, 'w')
150
- f.write(JSON.pretty_generate(content))
151
- f.close
63
+ cleared_params = params.split('&').select do |param|
64
+ param unless skip_query_params.include?(param.gsub(/=.*?$/, ''))
65
+ end.join('&')
152
66
 
153
- content[:body] = body
67
+ [url_path, cleared_params].join('?')
154
68
  end
155
69
 
156
- def safe_dir(url)
157
- if url.size > 225
158
- path = url.gsub(/(\?.*)/, '')
159
- params = Digest::SHA1.hexdigest Regexp.last_match(1)
160
- "#{path.gsub(%r{/}, '-').gsub(/^-/, '')}?#{params}"
161
- else
162
- url.gsub(%r{/}, '-').gsub(/^-/, '')
163
- end
70
+ def method
71
+ @options[:method].downcase
164
72
  end
165
73
  end
166
74
  end