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 +4 -4
- data/README.md +103 -35
- data/config/tshield.yml +6 -4
- data/lib/tshield.rb +1 -0
- data/lib/tshield/configuration.rb +3 -1
- data/lib/tshield/controllers/helpers/session_helpers.rb +15 -0
- data/lib/tshield/controllers/requests.rb +23 -25
- data/lib/tshield/controllers/sessions.rb +19 -6
- data/lib/tshield/extensions/string_extensions.rb +10 -0
- data/lib/tshield/options.rb +8 -28
- data/lib/tshield/request.rb +29 -121
- data/lib/tshield/request_matching.rb +122 -0
- data/lib/tshield/request_vcr.rb +139 -0
- data/lib/tshield/response.rb +3 -1
- data/lib/tshield/server.rb +2 -2
- data/lib/tshield/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/tshield/configuration_spec.rb +6 -0
- data/spec/tshield/fixtures/config/tshield.yml +2 -0
- data/spec/tshield/fixtures/matching/example.json +55 -0
- data/spec/tshield/options_spec.rb +38 -0
- data/spec/tshield/request_matching_spec.rb +137 -0
- data/spec/tshield/request_vcr_spec.rb +101 -0
- data/tshield.gemspec +4 -0
- metadata +94 -4
- data/spec/tshield/request_spec.rb +0 -52
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tshield/request'
|
4
|
+
|
5
|
+
DEFAULT_SESSION = 'no-session'
|
6
|
+
|
7
|
+
module TShield
|
8
|
+
# Class to check request matching
|
9
|
+
class RequestMatching
|
10
|
+
attr_reader :matched
|
11
|
+
|
12
|
+
def initialize(path, options = {})
|
13
|
+
super()
|
14
|
+
@path = path
|
15
|
+
@options = options
|
16
|
+
@options[:session] ||= DEFAULT_SESSION
|
17
|
+
@options[:method] ||= 'GET'
|
18
|
+
|
19
|
+
klass = self.class
|
20
|
+
klass.load_stubs unless klass.stubs
|
21
|
+
end
|
22
|
+
|
23
|
+
def match_request
|
24
|
+
@matched = find_stub
|
25
|
+
return unless matched
|
26
|
+
|
27
|
+
TShield::Response.new(matched['body'],
|
28
|
+
matched['headers'],
|
29
|
+
matched['status'])
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def find_stub
|
35
|
+
stubs = self.class.stubs
|
36
|
+
result = filter_stubs(stubs[@options[:session]])
|
37
|
+
return result if result
|
38
|
+
|
39
|
+
filter_stubs(stubs[DEFAULT_SESSION]) unless @options[:session] == DEFAULT_SESSION
|
40
|
+
end
|
41
|
+
|
42
|
+
def filter_by_method(stubs)
|
43
|
+
stubs.select { |stub| stub['method'] == @options[:method] }
|
44
|
+
end
|
45
|
+
|
46
|
+
def filter_by_headers(stubs)
|
47
|
+
stubs.select { |stub| self.class.include_headers(stub['headers'], @options[:headers]) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def filter_by_query(stubs)
|
51
|
+
stubs.select { |stub| self.class.include_query(stub['query'], @options[:raw_query] || '') }
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter_stubs(stubs)
|
55
|
+
result = filter_by_query(filter_by_headers(filter_by_method(stubs[@path] || [])))
|
56
|
+
result[0]['response'] unless result.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
attr_reader :stubs
|
61
|
+
|
62
|
+
def load_stubs
|
63
|
+
@stubs = {}
|
64
|
+
Dir.glob('matching/**/*.json').each do |entry|
|
65
|
+
load_stub(entry)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def load_stub(file)
|
70
|
+
content = JSON.parse File.open(file).read
|
71
|
+
content.each do |stub|
|
72
|
+
stub_session_name = init_stub_session(stub)
|
73
|
+
|
74
|
+
if stub['stubs']
|
75
|
+
load_items(stub['stubs'] || [], stub_session_name)
|
76
|
+
else
|
77
|
+
load_item(stub, stub_session_name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def init_stub_session(stub)
|
83
|
+
stub_session_name = stub['session'] || DEFAULT_SESSION
|
84
|
+
stubs[stub_session_name] ||= {}
|
85
|
+
stub_session_name
|
86
|
+
end
|
87
|
+
|
88
|
+
def load_items(items, session_name)
|
89
|
+
items.each { |item| load_item(item, session_name) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def load_item(item, session_name)
|
93
|
+
stubs[session_name][item['path']] ||= []
|
94
|
+
stubs[session_name][item['path']] << item
|
95
|
+
end
|
96
|
+
|
97
|
+
def include_headers(stub_headers, request_headers)
|
98
|
+
request_headers ||= {}
|
99
|
+
stub_headers ||= {}
|
100
|
+
result = stub_headers.reject { |key, value| request_headers[key.to_rack_name] == value }
|
101
|
+
result.empty? || stub_headers.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
def include_query(stub_query, raw_query)
|
105
|
+
request_query = build_query_hash(raw_query)
|
106
|
+
stub_query ||= {}
|
107
|
+
result = stub_query.reject { |key, value| request_query[key] == value.to_s }
|
108
|
+
result.empty? || stub_query.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_query_hash(raw_query)
|
112
|
+
params = {}
|
113
|
+
raw_query.split('&').each do |query|
|
114
|
+
key, value = query.split('=')
|
115
|
+
params[key] = value
|
116
|
+
end
|
117
|
+
|
118
|
+
params
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
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/request'
|
12
|
+
require 'tshield/response'
|
13
|
+
|
14
|
+
module TShield
|
15
|
+
# Module to write and read saved responses
|
16
|
+
class RequestVCR < TShield::Request
|
17
|
+
attr_reader :response
|
18
|
+
|
19
|
+
def initialize(path, options = {})
|
20
|
+
super()
|
21
|
+
@path = path
|
22
|
+
@options = options
|
23
|
+
|
24
|
+
request_configuration = configuration.request
|
25
|
+
@options[:timeout] = request_configuration['timeout']
|
26
|
+
@options[:verify] = request_configuration['verify_ssl']
|
27
|
+
request
|
28
|
+
end
|
29
|
+
|
30
|
+
def request
|
31
|
+
unless @options[:raw_query].nil? || @options[:raw_query].empty?
|
32
|
+
@path = "#{@path}?#{@options[:raw_query]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
@url = "#{domain}#{@path}"
|
36
|
+
|
37
|
+
if exists
|
38
|
+
@response = current_response
|
39
|
+
@response.original = false
|
40
|
+
else
|
41
|
+
@method = method
|
42
|
+
configuration.get_before_filters(domain).each do |filter|
|
43
|
+
@method, @url, @options = filter.new.filter(@method, @url, @options)
|
44
|
+
end
|
45
|
+
|
46
|
+
raw = HTTParty.send(@method.to_s, @url, @options)
|
47
|
+
|
48
|
+
configuration.get_after_filters(domain).each do |filter|
|
49
|
+
raw = filter.new.filter(raw)
|
50
|
+
end
|
51
|
+
|
52
|
+
@response = save(raw)
|
53
|
+
|
54
|
+
@response.original = true
|
55
|
+
end
|
56
|
+
current_session[:counter].add(@path, method) if current_session
|
57
|
+
@response
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def domain
|
63
|
+
@domain ||= configuration.get_domain_for(@path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def name
|
67
|
+
@name ||= configuration.get_name(domain)
|
68
|
+
end
|
69
|
+
|
70
|
+
def save(raw_response)
|
71
|
+
headers = {}
|
72
|
+
raw_response.headers.each do |k, v|
|
73
|
+
headers[k] = v unless configuration.not_save_headers(domain).include? k
|
74
|
+
end
|
75
|
+
|
76
|
+
content = {
|
77
|
+
body: raw_response.body,
|
78
|
+
status: raw_response.code,
|
79
|
+
headers: headers
|
80
|
+
}
|
81
|
+
|
82
|
+
write(content)
|
83
|
+
|
84
|
+
TShield::Response.new(raw_response.body, headers, raw_response.code)
|
85
|
+
end
|
86
|
+
|
87
|
+
def saved_content
|
88
|
+
return @saved_content if @saved_content
|
89
|
+
|
90
|
+
@saved_content = JSON.parse(File.open(headers_destiny).read)
|
91
|
+
@saved_content['body'] = File.open(content_destiny).read unless @saved_content['body']
|
92
|
+
@saved_content
|
93
|
+
end
|
94
|
+
|
95
|
+
def file_exists
|
96
|
+
session = current_session
|
97
|
+
@content_idx = session ? session[:counter].current(@path, method) : 0
|
98
|
+
File.exist?(content_destiny)
|
99
|
+
end
|
100
|
+
|
101
|
+
def exists
|
102
|
+
file_exists && configuration.cache_request?(domain)
|
103
|
+
end
|
104
|
+
|
105
|
+
def current_response
|
106
|
+
TShield::Response.new(saved_content['body'],
|
107
|
+
saved_content['headers'] || [],
|
108
|
+
saved_content['status'] || 200)
|
109
|
+
end
|
110
|
+
|
111
|
+
def key
|
112
|
+
@key ||= Digest::SHA1.hexdigest "#{@url}|#{method}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def write(content)
|
116
|
+
content_file = File.open(content_destiny, 'w')
|
117
|
+
content_file.write(content[:body])
|
118
|
+
content_file.close
|
119
|
+
|
120
|
+
body = content.delete :body
|
121
|
+
|
122
|
+
headers_file = File.open(headers_destiny, 'w')
|
123
|
+
headers_file.write(JSON.pretty_generate(content))
|
124
|
+
headers_file.close
|
125
|
+
|
126
|
+
content[:body] = body
|
127
|
+
end
|
128
|
+
|
129
|
+
def safe_dir(url)
|
130
|
+
if url.size > 225
|
131
|
+
path = url.gsub(/(\?.*)/, '')
|
132
|
+
params = Digest::SHA1.hexdigest Regexp.last_match(1)
|
133
|
+
"#{path.gsub(%r{/}, '-').gsub(/^-/, '')}?#{params}"
|
134
|
+
else
|
135
|
+
url.gsub(%r{/}, '-').gsub(/^-/, '')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/tshield/response.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TShield
|
4
|
+
# Response Object
|
4
5
|
class Response
|
5
|
-
attr_accessor :
|
6
|
+
attr_accessor :original
|
7
|
+
attr_reader :body, :headers, :status
|
6
8
|
|
7
9
|
def initialize(body, headers, status)
|
8
10
|
@body = body
|
data/lib/tshield/server.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'sinatra'
|
4
4
|
require 'haml'
|
5
5
|
|
6
|
+
require 'tshield/options'
|
6
7
|
require 'tshield/controllers/requests'
|
7
8
|
require 'tshield/controllers/sessions'
|
8
9
|
|
@@ -15,6 +16,7 @@ module TShield
|
|
15
16
|
set :public_dir, File.join(File.dirname(__FILE__), 'assets')
|
16
17
|
set :views, File.join(File.dirname(__FILE__), 'views')
|
17
18
|
set :bind, '0.0.0.0'
|
19
|
+
set :port, TShield::Options.instance.port
|
18
20
|
|
19
21
|
def self.register_resources
|
20
22
|
load_controllers
|
@@ -26,8 +28,6 @@ module TShield
|
|
26
28
|
return unless File.exist?('controllers')
|
27
29
|
|
28
30
|
Dir.entries('controllers').each do |entry|
|
29
|
-
require 'byebug'
|
30
|
-
debugger
|
31
31
|
next if entry =~ /^\.\.?$/
|
32
32
|
|
33
33
|
entry.gsub!('.rb', '')
|
data/lib/tshield/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -29,6 +29,12 @@ describe TShield::Configuration do
|
|
29
29
|
)
|
30
30
|
end
|
31
31
|
|
32
|
+
it 'recover skip query params' do
|
33
|
+
expect(@configuration.domains['example.org']['skip_query_params']).to(
|
34
|
+
include('a')
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
32
38
|
context 'on load filters' do
|
33
39
|
it 'recover filters for a domain' do
|
34
40
|
expect(@configuration.get_filters('example.org')).to eq([ExampleFilter])
|
@@ -0,0 +1,55 @@
|
|
1
|
+
[{
|
2
|
+
"method": "GET",
|
3
|
+
"path": "/matching/example",
|
4
|
+
"query": {
|
5
|
+
"query1": "value"
|
6
|
+
},
|
7
|
+
"response": {
|
8
|
+
"status": 200,
|
9
|
+
"headers": {},
|
10
|
+
"body": "query content"
|
11
|
+
}
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"method": "GET",
|
15
|
+
"path": "/matching/example",
|
16
|
+
"response": {
|
17
|
+
"status": 200,
|
18
|
+
"headers": {},
|
19
|
+
"body": "body content"
|
20
|
+
}
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"method": "POST",
|
24
|
+
"path": "/matching/example",
|
25
|
+
"headers": {
|
26
|
+
"header1": "value"
|
27
|
+
},
|
28
|
+
"response": {
|
29
|
+
"status": 201,
|
30
|
+
"headers": {},
|
31
|
+
"body": "headers content"
|
32
|
+
}
|
33
|
+
},
|
34
|
+
{
|
35
|
+
"method": "POST",
|
36
|
+
"path": "/matching/example",
|
37
|
+
"response": {
|
38
|
+
"status": 201,
|
39
|
+
"headers": {},
|
40
|
+
"body": "post content"
|
41
|
+
}
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"session": "a-session",
|
45
|
+
"stubs": [{
|
46
|
+
"method": "GET",
|
47
|
+
"path": "/matching/example",
|
48
|
+
"response": {
|
49
|
+
"status": 200,
|
50
|
+
"headers": {},
|
51
|
+
"body": "body content in session"
|
52
|
+
}
|
53
|
+
}]
|
54
|
+
}
|
55
|
+
]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'tshield/options'
|
5
|
+
require 'spec_helper'
|
6
|
+
|
7
|
+
describe TShield::Options do
|
8
|
+
context 'with parsing' do
|
9
|
+
before :each do
|
10
|
+
options_parser = double
|
11
|
+
@opts = double
|
12
|
+
|
13
|
+
allow(OptionParser).to receive(:new)
|
14
|
+
.and_return(options_parser)
|
15
|
+
.and_yield(@opts)
|
16
|
+
|
17
|
+
allow(options_parser).to receive(:parse!)
|
18
|
+
|
19
|
+
allow(@opts).to receive(:banner=)
|
20
|
+
allow(@opts).to receive(:on)
|
21
|
+
allow(@opts).to receive(:on_tail)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should recover default port' do
|
25
|
+
TShield::Options.init
|
26
|
+
expect(TShield::Options.instance.port).to eql(4567)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should recover custom port' do
|
30
|
+
allow(@opts).to receive(:on)
|
31
|
+
.with('-p', '--port [PORT]', 'Sinatra port')
|
32
|
+
.and_yield('4568')
|
33
|
+
|
34
|
+
TShield::Options.init
|
35
|
+
expect(TShield::Options.instance.port).to eql(4568)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
require 'tshield/configuration'
|
6
|
+
require 'tshield/request_matching'
|
7
|
+
require 'tshield/response'
|
8
|
+
|
9
|
+
describe TShield::RequestMatching do
|
10
|
+
before :each do
|
11
|
+
@configuration = double
|
12
|
+
allow(TShield::Configuration)
|
13
|
+
.to receive(:singleton).and_return(@configuration)
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'matching path' do
|
17
|
+
before :each do
|
18
|
+
allow(Dir).to receive(:glob)
|
19
|
+
.and_return(['spec/tshield/fixtures/matching/example.json'])
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'on loading stubs' do
|
23
|
+
before :each do
|
24
|
+
@request_matching = TShield::RequestMatching.new('/')
|
25
|
+
end
|
26
|
+
context 'for path /matching/example' do
|
27
|
+
it 'should map path' do
|
28
|
+
expect(@request_matching.class.stubs[DEFAULT_SESSION]).to include('/matching/example')
|
29
|
+
end
|
30
|
+
context 'on settings' do
|
31
|
+
before :each do
|
32
|
+
@entry = @request_matching.class.stubs[DEFAULT_SESSION]['/matching/example'][0]
|
33
|
+
end
|
34
|
+
it 'should answer for the method GET' do
|
35
|
+
expect(@entry['method']).to include('GET')
|
36
|
+
end
|
37
|
+
it 'should have response body' do
|
38
|
+
expect(@entry['response']['body']).to include('query content')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'on matching request' do
|
45
|
+
context 'on match' do
|
46
|
+
before :each do
|
47
|
+
@request_matching = TShield::RequestMatching.new('/matching/example', method: 'GET')
|
48
|
+
end
|
49
|
+
it 'should return response object' do
|
50
|
+
@response = @request_matching.match_request
|
51
|
+
expect(@response.body).to eql('body content')
|
52
|
+
expect(@response.headers).to eql({})
|
53
|
+
expect(@response.status).to eql(200)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
context 'on match by path and method' do
|
57
|
+
before :each do
|
58
|
+
@request_matching = TShield::RequestMatching
|
59
|
+
.new('/matching/example', method: 'POST')
|
60
|
+
end
|
61
|
+
it 'should return response object' do
|
62
|
+
@response = @request_matching.match_request
|
63
|
+
expect(@response.body).to eql('post content')
|
64
|
+
expect(@response.headers).to eql({})
|
65
|
+
expect(@response.status).to eql(201)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
context 'on match by path and method and headers' do
|
69
|
+
before :each do
|
70
|
+
@request_matching = TShield::RequestMatching
|
71
|
+
.new('/matching/example',
|
72
|
+
method: 'POST',
|
73
|
+
headers: { 'HTTP_HEADER1' => 'value' })
|
74
|
+
end
|
75
|
+
it 'should return response object' do
|
76
|
+
@response = @request_matching.match_request
|
77
|
+
expect(@response.body).to eql('headers content')
|
78
|
+
expect(@response.headers).to eql({})
|
79
|
+
expect(@response.status).to eql(201)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
context 'on match by path and method and query' do
|
83
|
+
before :each do
|
84
|
+
@request_matching = TShield::RequestMatching
|
85
|
+
.new('/matching/example',
|
86
|
+
method: 'GET',
|
87
|
+
raw_query: 'query1=value')
|
88
|
+
end
|
89
|
+
it 'should return response object' do
|
90
|
+
@response = @request_matching.match_request
|
91
|
+
expect(@response.body).to eql('query content')
|
92
|
+
expect(@response.headers).to eql({})
|
93
|
+
expect(@response.status).to eql(200)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
context 'on not match' do
|
97
|
+
before :each do
|
98
|
+
@request_matching = TShield::RequestMatching.new('/')
|
99
|
+
end
|
100
|
+
it 'should return nil' do
|
101
|
+
expect(@request_matching.match_request).to be_nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
context 'on session' do
|
106
|
+
context 'load session infos' do
|
107
|
+
before :each do
|
108
|
+
@request_matching = TShield::RequestMatching.new('/')
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should map path' do
|
112
|
+
expect(@request_matching.class.stubs['a-session']).to include('/matching/example')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
context 'on match' do
|
116
|
+
it 'should return response object from session settings' do
|
117
|
+
@request_matching = TShield::RequestMatching.new('/matching/example',
|
118
|
+
method: 'GET',
|
119
|
+
session: 'a-session')
|
120
|
+
@response = @request_matching.match_request
|
121
|
+
expect(@response.body).to eql('body content in session')
|
122
|
+
expect(@response.headers).to eql({})
|
123
|
+
expect(@response.status).to eql(200)
|
124
|
+
end
|
125
|
+
it 'should return response object from default settings' do
|
126
|
+
@request_matching = TShield::RequestMatching.new('/matching/example',
|
127
|
+
method: 'POST',
|
128
|
+
session: 'a-session')
|
129
|
+
@response = @request_matching.match_request
|
130
|
+
expect(@response.body).to eql('post content')
|
131
|
+
expect(@response.headers).to eql({})
|
132
|
+
expect(@response.status).to eql(201)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|