tshield 0.10.0.0 → 0.11.0.0

Sign up to get free protection for your applications and to get access to all the features.
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