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 +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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 618a5c8f2527911612734f7aaddd21fab7ba82066409a451ad4bd39ac42eae33
|
4
|
+
data.tar.gz: 4637697bcef288f4276e664adb653eaa7c6aae7b898cdf8b238ac27a8499b55c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/tshield.rb
CHANGED
@@ -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 '
|
6
|
-
|
5
|
+
require 'tshield/controllers/helpers/session_helpers'
|
7
6
|
require 'tshield/options'
|
8
7
|
require 'tshield/configuration'
|
9
|
-
require 'tshield/
|
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:
|
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
|
-
|
80
|
+
unless api_response
|
81
|
+
add_headers(headers, path)
|
78
82
|
|
79
|
-
|
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}
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
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
|
data/lib/tshield/options.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
52
|
-
opts.on('-
|
53
|
-
'
|
54
|
-
@options[:
|
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
|
data/lib/tshield/request.rb
CHANGED
@@ -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 :
|
8
|
+
attr_reader :configuration
|
9
|
+
attr_writer :content_idx
|
17
10
|
|
18
|
-
def initialize
|
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
|
-
|
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
|
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
|
-
|
104
|
-
File.exist?(destiny)
|
105
|
-
end
|
23
|
+
return unless session
|
106
24
|
|
107
|
-
|
108
|
-
|
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
|
112
|
-
|
29
|
+
def content_destiny
|
30
|
+
"#{destiny}.content"
|
113
31
|
end
|
114
32
|
|
115
|
-
def
|
116
|
-
|
33
|
+
def headers_destiny
|
34
|
+
"#{destiny}.json"
|
117
35
|
end
|
118
36
|
|
119
|
-
def destiny
|
37
|
+
def destiny
|
120
38
|
request_path = File.join('requests')
|
121
39
|
Dir.mkdir(request_path) unless File.exist?(request_path)
|
122
40
|
|
123
|
-
|
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
|
-
|
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
|
-
|
139
|
-
File.join(method_path, destiny_name)
|
54
|
+
File.join(method_path, @content_idx.to_s)
|
140
55
|
end
|
141
56
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
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
|
-
|
61
|
+
return path if !skip_query_params || !params
|
148
62
|
|
149
|
-
|
150
|
-
|
151
|
-
|
63
|
+
cleared_params = params.split('&').select do |param|
|
64
|
+
param unless skip_query_params.include?(param.gsub(/=.*?$/, ''))
|
65
|
+
end.join('&')
|
152
66
|
|
153
|
-
|
67
|
+
[url_path, cleared_params].join('?')
|
154
68
|
end
|
155
69
|
|
156
|
-
def
|
157
|
-
|
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
|