tshield 0.8.0.0 → 0.9.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 +5 -5
- data/Gemfile +3 -2
- data/README.md +147 -4
- data/Rakefile +13 -2
- data/bin/tshield +5 -5
- data/config/tshield.yml +9 -0
- data/lib/tshield/after_filter.rb +3 -2
- data/lib/tshield/before_filter.rb +3 -2
- data/lib/tshield/configuration.rb +57 -36
- data/lib/tshield/controller.rb +22 -10
- data/lib/tshield/controllers/requests.rb +20 -21
- data/lib/tshield/controllers/sessions.rb +2 -3
- data/lib/tshield/counter.rb +5 -5
- data/lib/tshield/logger.rb +10 -0
- data/lib/tshield/options.rb +61 -27
- data/lib/tshield/request.rb +25 -28
- data/lib/tshield/response.rb +2 -0
- data/lib/tshield/server.rb +24 -19
- data/lib/tshield/sessions.rb +6 -4
- data/lib/tshield/simple_tcp_server.rb +3 -2
- data/lib/tshield/version.rb +4 -2
- data/lib/tshield.rb +3 -2
- data/spec/spec_helper.rb +6 -6
- data/spec/tshield/after_filter_spec.rb +7 -0
- data/spec/tshield/configuration_spec.rb +57 -20
- data/spec/tshield/fixtures/config/tshield.yml +7 -1
- data/spec/tshield/fixtures/filters/example_filter.rb +9 -0
- data/spec/tshield/request_spec.rb +43 -2
- data/tshield.gemspec +28 -22
- metadata +139 -67
- data/lib/tshield/assets/favicon.ico +0 -0
- data/lib/tshield/assets/javascripts/application.js +0 -0
- data/lib/tshield/assets/javascripts/bootstrap.min.js +0 -7
- data/lib/tshield/assets/javascripts/jquery.min.js +0 -4
- data/lib/tshield/assets/stylesheets/application.css +0 -49
- data/lib/tshield/assets/stylesheets/bootstrap-theme.min.css +0 -6
- data/lib/tshield/assets/stylesheets/bootstrap.min.css +0 -6
- data/lib/tshield/controllers/admin/requests.rb +0 -62
- data/lib/tshield/controllers/admin/sessions.rb +0 -40
- data/lib/tshield/views/admin/requests/index.haml +0 -6
- data/lib/tshield/views/admin/requests/show.haml +0 -25
- data/lib/tshield/views/admin/sessions/index.haml +0 -6
- data/lib/tshield/views/layout/base.haml +0 -15
data/lib/tshield/options.rb
CHANGED
@@ -1,21 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
|
5
|
+
require 'tshield/logger'
|
3
6
|
require 'tshield/version'
|
4
7
|
|
5
8
|
module TShield
|
9
|
+
# Options for command line
|
6
10
|
class Options
|
7
|
-
|
8
11
|
attr_reader :debug
|
9
12
|
|
10
13
|
def self.init
|
11
|
-
|
14
|
+
@instance = TShield::Options.new
|
12
15
|
end
|
13
16
|
|
14
17
|
def self.instance
|
15
|
-
|
18
|
+
@instance || TShield::Options.new
|
16
19
|
end
|
17
20
|
|
18
21
|
def initialize
|
22
|
+
@options = {}
|
19
23
|
parse
|
20
24
|
end
|
21
25
|
|
@@ -23,42 +27,72 @@ module TShield
|
|
23
27
|
check_breakpoint(args)
|
24
28
|
end
|
25
29
|
|
30
|
+
def configuration_file
|
31
|
+
@options.fetch(:configuration_file, 'config/tshield.yml')
|
32
|
+
end
|
33
|
+
|
26
34
|
private
|
35
|
+
|
27
36
|
def check_breakpoint(args)
|
28
|
-
check_breakpoint_moment(args)
|
37
|
+
check_breakpoint_moment(args)
|
29
38
|
end
|
30
39
|
|
31
40
|
def check_breakpoint_moment(args)
|
32
41
|
@options["#{args[:moment]}_pattern".to_sym] =~ args[:path]
|
33
42
|
end
|
34
43
|
|
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
|
50
|
+
|
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)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def register_configuration(opts)
|
59
|
+
opts.on('-c', '--configuration [FILE]',
|
60
|
+
'Configuration File') do |file|
|
61
|
+
@options[:configuration_file] = file
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def register_patterns(opts)
|
66
|
+
register_before_pattern(opts)
|
67
|
+
register_after_pattern(opts)
|
68
|
+
end
|
69
|
+
|
70
|
+
def register_version(opts)
|
71
|
+
opts.on('-v', '--version', 'Show version') do
|
72
|
+
TShield.logger.info(TShield::Version.to_s)
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def register_help(opts)
|
78
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
79
|
+
puts opts
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
35
84
|
def parse
|
36
|
-
@options = {}
|
37
85
|
OptionParser.new do |opts|
|
38
|
-
opts.banner =
|
39
|
-
|
40
|
-
opts.on('-b', '--break-before-request [PATTERN]',
|
41
|
-
'Breakpoint before request') do |pattern|
|
42
|
-
@options[:before_pattern] = Regexp.new(pattern)
|
43
|
-
end
|
44
|
-
|
45
|
-
opts.on('-a', '--break-after-request [PATTERN]',
|
46
|
-
'Breakpoint after request') do |pattern|
|
47
|
-
@options[:after_pattern] = Regexp.new(pattern)
|
48
|
-
end
|
49
|
-
|
50
|
-
opts.on("-v", "--version", "Show version") do
|
51
|
-
puts TShield::Version
|
52
|
-
exit
|
53
|
-
end
|
54
|
-
|
55
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
56
|
-
puts opts
|
57
|
-
exit
|
58
|
-
end
|
86
|
+
opts.banner = 'Usage: tshield [options]'
|
87
|
+
register(opts)
|
59
88
|
end.parse!
|
60
89
|
end
|
61
90
|
|
91
|
+
def register(opts)
|
92
|
+
register_configuration(opts)
|
93
|
+
register_patterns(opts)
|
94
|
+
register_version(opts)
|
95
|
+
register_help(opts)
|
96
|
+
end
|
62
97
|
end
|
63
98
|
end
|
64
|
-
|
data/lib/tshield/request.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'httparty'
|
2
4
|
require 'json'
|
3
5
|
require 'byebug'
|
@@ -10,29 +12,27 @@ require 'tshield/response'
|
|
10
12
|
require 'tshield/sessions'
|
11
13
|
|
12
14
|
module TShield
|
13
|
-
|
14
15
|
class Request
|
15
|
-
|
16
16
|
attr_reader :response
|
17
17
|
|
18
18
|
def initialize(path, options = {})
|
19
19
|
@path = path
|
20
|
-
@options = options
|
20
|
+
@options = options
|
21
21
|
@configuration = TShield::Configuration.singleton
|
22
|
-
@options[:timeout] =
|
23
|
-
@options[:verify] =
|
22
|
+
@options[:timeout] = @configuration.request['timeout']
|
23
|
+
@options[:verify] = @configuration.request['verify_ssl']
|
24
24
|
request
|
25
25
|
end
|
26
26
|
|
27
27
|
def request
|
28
|
-
|
28
|
+
unless @options[:raw_query].nil? || @options[:raw_query].empty?
|
29
29
|
@path = "#{@path}?#{@options[:raw_query]}"
|
30
30
|
end
|
31
31
|
|
32
32
|
@url = "#{domain}#{@path}"
|
33
33
|
|
34
34
|
if exists
|
35
|
-
@response = get_current_response
|
35
|
+
@response = get_current_response
|
36
36
|
@response.original = false
|
37
37
|
else
|
38
38
|
@method = method
|
@@ -40,7 +40,7 @@ module TShield
|
|
40
40
|
@method, @url, @options = filter.new.filter(@method, @url, @options)
|
41
41
|
end
|
42
42
|
|
43
|
-
raw = HTTParty.send(
|
43
|
+
raw = HTTParty.send(@method.to_s, @url, @options)
|
44
44
|
|
45
45
|
@configuration.get_after_filters(domain).each do |filter|
|
46
46
|
raw = filter.new.filter(raw)
|
@@ -56,6 +56,7 @@ module TShield
|
|
56
56
|
end
|
57
57
|
|
58
58
|
private
|
59
|
+
|
59
60
|
def domain
|
60
61
|
@domain ||= @configuration.get_domain_for(@path)
|
61
62
|
end
|
@@ -70,14 +71,12 @@ module TShield
|
|
70
71
|
|
71
72
|
def save(raw_response)
|
72
73
|
headers = {}
|
73
|
-
raw_response.headers.each do |k,v|
|
74
|
-
|
75
|
-
headers[k] = v
|
76
|
-
end
|
74
|
+
raw_response.headers.each do |k, v|
|
75
|
+
headers[k] = v unless @configuration.not_save_headers(domain).include? k
|
77
76
|
end
|
78
77
|
|
79
78
|
content = {
|
80
|
-
body: raw_response.body,
|
79
|
+
body: raw_response.body,
|
81
80
|
status: raw_response.code,
|
82
81
|
headers: headers
|
83
82
|
}
|
@@ -102,7 +101,7 @@ module TShield
|
|
102
101
|
def file_exists
|
103
102
|
session = current_session
|
104
103
|
@content_idx = session ? session[:counter].current(@path, method) : 0
|
105
|
-
File.
|
104
|
+
File.exist?(destiny)
|
106
105
|
end
|
107
106
|
|
108
107
|
def exists
|
@@ -110,7 +109,7 @@ module TShield
|
|
110
109
|
end
|
111
110
|
|
112
111
|
def get_current_response
|
113
|
-
TShield::Response.new(content['body'], content['headers'] || [], content['status'] || 200)
|
112
|
+
TShield::Response.new(content['body'], content['headers'] || [], content['status'] || 200)
|
114
113
|
end
|
115
114
|
|
116
115
|
def key
|
@@ -119,22 +118,23 @@ module TShield
|
|
119
118
|
|
120
119
|
def destiny(iscontent = false)
|
121
120
|
request_path = File.join('requests')
|
122
|
-
Dir.mkdir(request_path) unless File.
|
121
|
+
Dir.mkdir(request_path) unless File.exist?(request_path)
|
123
122
|
|
124
|
-
|
123
|
+
session = current_session
|
124
|
+
if session
|
125
125
|
request_path = File.join(request_path, session[:name])
|
126
|
-
Dir.mkdir(request_path) unless File.
|
126
|
+
Dir.mkdir(request_path) unless File.exist?(request_path)
|
127
127
|
end
|
128
128
|
|
129
129
|
name_path = File.join(request_path, name)
|
130
|
-
Dir.mkdir(name_path) unless File.
|
130
|
+
Dir.mkdir(name_path) unless File.exist?(name_path)
|
131
131
|
|
132
132
|
path_path = File.join(name_path, safe_dir(@path))
|
133
|
-
Dir.mkdir(path_path) unless File.
|
133
|
+
Dir.mkdir(path_path) unless File.exist?(path_path)
|
134
134
|
|
135
135
|
method_path = File.join(path_path, method)
|
136
|
-
Dir.mkdir(method_path) unless File.
|
137
|
-
|
136
|
+
Dir.mkdir(method_path) unless File.exist?(method_path)
|
137
|
+
|
138
138
|
destiny_name = iscontent ? "#{@content_idx}.content" : "#{@content_idx}.json"
|
139
139
|
File.join(method_path, destiny_name)
|
140
140
|
end
|
@@ -156,14 +156,11 @@ module TShield
|
|
156
156
|
def safe_dir(url)
|
157
157
|
if url.size > 225
|
158
158
|
path = url.gsub(/(\?.*)/, '')
|
159
|
-
params = Digest::SHA1.hexdigest
|
160
|
-
"#{path.gsub(
|
159
|
+
params = Digest::SHA1.hexdigest Regexp.last_match(1)
|
160
|
+
"#{path.gsub(%r{/}, '-').gsub(/^-/, '')}?#{params}"
|
161
161
|
else
|
162
|
-
url.gsub(
|
162
|
+
url.gsub(%r{/}, '-').gsub(/^-/, '')
|
163
163
|
end
|
164
164
|
end
|
165
|
-
|
166
165
|
end
|
167
|
-
|
168
166
|
end
|
169
|
-
|
data/lib/tshield/response.rb
CHANGED
data/lib/tshield/server.rb
CHANGED
@@ -1,22 +1,35 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'sinatra'
|
2
4
|
require 'haml'
|
3
5
|
|
4
6
|
require 'tshield/controllers/requests'
|
5
7
|
require 'tshield/controllers/sessions'
|
6
8
|
|
7
|
-
require 'tshield/controllers/admin/requests'
|
8
|
-
require 'tshield/controllers/admin/sessions'
|
9
|
-
|
10
9
|
module TShield
|
10
|
+
# Base of TShield Server
|
11
11
|
class Server < Sinatra::Base
|
12
|
-
|
13
12
|
include TShield::Controllers::Requests::Helpers
|
14
|
-
include TShield::Controllers::Admin::Sessions::Helpers
|
15
|
-
include TShield::Controllers::Admin::Requests::Helpers
|
16
13
|
|
17
|
-
|
14
|
+
set :protection, except: [:json_csrf]
|
15
|
+
set :public_dir, File.join(File.dirname(__FILE__), 'assets')
|
16
|
+
set :views, File.join(File.dirname(__FILE__), 'views')
|
17
|
+
set :bind, '0.0.0.0'
|
18
|
+
|
19
|
+
def self.register_resources
|
20
|
+
load_controllers
|
21
|
+
register TShield::Controllers::Sessions
|
22
|
+
register TShield::Controllers::Requests
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load_controllers
|
26
|
+
return unless File.exist?('controllers')
|
27
|
+
|
18
28
|
Dir.entries('controllers').each do |entry|
|
29
|
+
require 'byebug'
|
30
|
+
debugger
|
19
31
|
next if entry =~ /^\.\.?$/
|
32
|
+
|
20
33
|
entry.gsub!('.rb', '')
|
21
34
|
require File.join('.', 'controllers', entry)
|
22
35
|
controller_name = entry.split('_').collect(&:capitalize).join
|
@@ -25,17 +38,9 @@ module TShield
|
|
25
38
|
end
|
26
39
|
end
|
27
40
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
register TShield::Controllers::Admin::Sessions
|
34
|
-
register TShield::Controllers::Admin::Requests
|
35
|
-
|
36
|
-
register TShield::Controllers::Sessions
|
37
|
-
register TShield::Controllers::Requests
|
38
|
-
|
41
|
+
def self.run!
|
42
|
+
register_resources
|
43
|
+
super.run!
|
44
|
+
end
|
39
45
|
end
|
40
46
|
end
|
41
|
-
|
data/lib/tshield/sessions.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'byebug'
|
2
4
|
require 'tshield/counter'
|
3
5
|
|
4
6
|
module TShield
|
7
|
+
# Manage sessions
|
8
|
+
#
|
9
|
+
# Start and stop session for ip
|
5
10
|
module Sessions
|
6
11
|
def self.start(ip, name)
|
7
|
-
sessions[normalize_ip(ip)] = {name: name, counter: TShield::Counter.new}
|
12
|
+
sessions[normalize_ip(ip)] = { name: name, counter: TShield::Counter.new }
|
8
13
|
end
|
9
14
|
|
10
15
|
def self.stop(ip)
|
@@ -15,7 +20,6 @@ module TShield
|
|
15
20
|
sessions[normalize_ip(ip)]
|
16
21
|
end
|
17
22
|
|
18
|
-
protected
|
19
23
|
def self.sessions
|
20
24
|
@sessions ||= {}
|
21
25
|
end
|
@@ -23,7 +27,5 @@ module TShield
|
|
23
27
|
def self.normalize_ip(ip)
|
24
28
|
ip == '::1' ? '127.0.0.1' : ip
|
25
29
|
end
|
26
|
-
|
27
30
|
end
|
28
31
|
end
|
29
|
-
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
|
3
5
|
module TShield
|
@@ -6,7 +8,7 @@ module TShield
|
|
6
8
|
@running = true
|
7
9
|
end
|
8
10
|
|
9
|
-
def on_connect(
|
11
|
+
def on_connect(_client)
|
10
12
|
raise 'should implement method on_connect'
|
11
13
|
end
|
12
14
|
|
@@ -24,4 +26,3 @@ module TShield
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
27
|
-
|
data/lib/tshield/version.rb
CHANGED
data/lib/tshield.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'bundler/setup'
|
2
4
|
Bundler.setup
|
3
5
|
|
@@ -5,14 +7,12 @@ require 'simplecov'
|
|
5
7
|
SimpleCov.start
|
6
8
|
|
7
9
|
require 'httparty'
|
8
|
-
require 'tshield'
|
9
|
-
|
10
10
|
require 'webmock/rspec'
|
11
11
|
|
12
12
|
RSpec.configure do |config|
|
13
|
-
|
14
|
-
|
13
|
+
config.before(:each) do
|
14
|
+
allow(File).to receive(:join).and_return(
|
15
|
+
'spec/tshield/fixtures/config/tshield.yml'
|
16
|
+
)
|
15
17
|
end
|
16
|
-
|
17
18
|
end
|
18
|
-
|
@@ -1,32 +1,69 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require 'tshield/configuration'
|
1
4
|
require 'spec_helper'
|
2
5
|
|
3
6
|
describe TShield::Configuration do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
context 'on config exist' do
|
8
|
+
before :each do
|
9
|
+
options_instance = double
|
10
|
+
allow(options_instance).to receive(:configuration_file)
|
11
|
+
.and_return('spec/tshield/fixtures/config/tshield.yml')
|
12
|
+
allow(TShield::Options).to receive(:instance).and_return(options_instance)
|
13
|
+
allow(File).to receive(:join).and_return(
|
14
|
+
'./spec/tshield/fixtures/filters/example_filter.rb'
|
15
|
+
)
|
16
|
+
allow(File).to receive(:exist?) do
|
17
|
+
true
|
18
|
+
end
|
19
|
+
allow(Dir).to receive(:entries) do
|
20
|
+
['.', '..', 'example_filter.rb']
|
21
|
+
end
|
22
|
+
@configuration = TShield::Configuration.singleton
|
14
23
|
end
|
15
|
-
end
|
16
24
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
context 'load configurations from yaml' do
|
26
|
+
it 'recover domains' do
|
27
|
+
expect(@configuration.domains['example.org']['paths']).to(
|
28
|
+
include('/api/one', '/api/two')
|
29
|
+
)
|
30
|
+
end
|
21
31
|
|
22
|
-
|
23
|
-
|
32
|
+
context 'on load filters' do
|
33
|
+
it 'recover filters for a domain' do
|
34
|
+
expect(@configuration.get_filters('example.org')).to eq([ExampleFilter])
|
35
|
+
end
|
36
|
+
it 'return empty array if domain not have filters' do
|
37
|
+
expect(@configuration.get_filters('example.com')).to eq([])
|
38
|
+
end
|
39
|
+
end
|
24
40
|
end
|
25
41
|
|
26
|
-
|
27
|
-
|
42
|
+
describe 'get_domain_for' do
|
43
|
+
it 'return domain for example.org' do
|
44
|
+
expect(@configuration.get_domain_for('/api/two')).to eq('example.org')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'return domain for example.com' do
|
48
|
+
expect(@configuration.get_domain_for('/api/three')).to eq('example.com')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'return nil if domain not found' do
|
52
|
+
expect(@configuration.get_domain_for('/api/four')).to be_nil
|
53
|
+
end
|
28
54
|
end
|
29
55
|
end
|
56
|
+
context 'on config not exist' do
|
57
|
+
before :each do
|
58
|
+
options_instance = double
|
59
|
+
allow(options_instance).to receive(:configuration_file)
|
60
|
+
.and_return('not_found/config/tshield.yml')
|
61
|
+
allow(TShield::Options).to receive(:instance).and_return(options_instance)
|
62
|
+
TShield::Configuration.clear
|
63
|
+
end
|
30
64
|
|
65
|
+
it 'exit with error status' do
|
66
|
+
expect { TShield::Configuration.singleton }.to raise_error RuntimeError
|
67
|
+
end
|
68
|
+
end
|
31
69
|
end
|
32
|
-
|
@@ -1,11 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
5
|
+
require 'tshield/request'
|
6
|
+
|
3
7
|
describe TShield::Request do
|
8
|
+
before :each do
|
9
|
+
configuration = double
|
10
|
+
allow(TShield::Configuration)
|
11
|
+
.to receive(:singleton).and_return(configuration)
|
12
|
+
allow(configuration).to receive(:get_before_filters).and_return([])
|
13
|
+
allow(configuration).to receive(:get_after_filters).and_return([])
|
14
|
+
allow(configuration).to receive(:request).and_return('timeout' => 10)
|
15
|
+
allow(configuration).to receive(:get_domain_for).and_return('example.org')
|
16
|
+
allow(TShield::Options).to receive_message_chain(:instance, :break?)
|
17
|
+
end
|
4
18
|
|
5
19
|
describe 'when save response' do
|
6
|
-
it '' do
|
20
|
+
it 'should write response body, request status and headers' do
|
21
|
+
allow_any_instance_of(TShield::Request).to receive(:exists)
|
22
|
+
.and_return(false)
|
23
|
+
allow_any_instance_of(TShield::Request).to receive(:destiny)
|
24
|
+
allow(HTTParty).to receive(:send).and_return(RawResponse.new)
|
25
|
+
|
26
|
+
write_spy = double
|
27
|
+
allow(File).to receive(:open).and_return(write_spy)
|
28
|
+
|
29
|
+
expect(write_spy).to receive(:write).ordered.with('this is the body')
|
30
|
+
expect(write_spy).to receive(:write)
|
31
|
+
.ordered
|
32
|
+
.with("{\n \"status\": 200,\n \"headers\": {\n }\n}")
|
33
|
+
allow(write_spy).to receive(:close)
|
34
|
+
|
35
|
+
TShield::Request.new '/', method: 'GET'
|
7
36
|
end
|
8
37
|
end
|
9
38
|
|
10
|
-
|
39
|
+
class RawResponse
|
40
|
+
def headers
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
def body
|
45
|
+
'this is the body'
|
46
|
+
end
|
11
47
|
|
48
|
+
def code
|
49
|
+
200
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|