rack-radar 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +283 -0
- data/Rakefile +14 -0
- data/lib/rack-radar.rb +1 -0
- data/lib/rack/radar.rb +127 -0
- data/lib/rack/radar/cookies.rb +128 -0
- data/lib/rack/radar/session.rb +190 -0
- data/rack-radar.gemspec +21 -0
- data/test/app_test.rb +52 -0
- data/test/auth_test.rb +150 -0
- data/test/cookies_test.rb +280 -0
- data/test/follow_redirect_test.rb +44 -0
- data/test/headers_test.rb +82 -0
- data/test/map_test.rb +64 -0
- data/test/params_test.rb +45 -0
- data/test/request_methods_test.rb +52 -0
- data/test/session_test.rb +42 -0
- data/test/test_setup.rb +162 -0
- metadata +105 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
module Rack
|
2
|
+
module Radar
|
3
|
+
class RackRadarCookies
|
4
|
+
include ::Rack::Utils
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@jar = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def jar uri, cookies = nil
|
11
|
+
host = (((uri && uri.host) || RACK_RADAR__DEFAULT_HOST).split('.')[-2..-1]||[]).join('.').downcase
|
12
|
+
@jar[host] = cookies if cookies
|
13
|
+
@jar[host] ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def [] name
|
17
|
+
cookies = to_hash
|
18
|
+
cookies[name] && cookies[name].value
|
19
|
+
end
|
20
|
+
|
21
|
+
def []= name, value
|
22
|
+
persist '%s=%s' % [name, ::Rack::Utils.escape(value)]
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete name
|
26
|
+
jar nil, jar(nil).reject { |c| c.name == name }
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear
|
30
|
+
jar nil, []
|
31
|
+
end
|
32
|
+
|
33
|
+
%w[size empty?].each do |m|
|
34
|
+
define_method m do |*args|
|
35
|
+
jar(nil).send __method__, *args
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def persist raw_cookies, uri = nil
|
40
|
+
return unless raw_cookies.is_a?(String)
|
41
|
+
|
42
|
+
# before adding new cookies, lets cleanup expired ones
|
43
|
+
jar uri, jar(uri).reject { |c| c.expired? }
|
44
|
+
|
45
|
+
raw_cookies = raw_cookies.strip.split("\n").reject { |c| c.empty? }
|
46
|
+
|
47
|
+
raw_cookies.each do |raw_cookie|
|
48
|
+
cookie = Cookie.new(raw_cookie, uri)
|
49
|
+
cookie.valid?(uri) || next
|
50
|
+
jar(uri, jar(uri).reject { |existing_cookie| cookie.replaces? existing_cookie })
|
51
|
+
jar(uri) << cookie
|
52
|
+
end
|
53
|
+
jar(uri).sort!
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s uri = nil
|
57
|
+
to_hash(uri).values.map { |c| c.raw }.join(';')
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_hash uri = nil
|
61
|
+
jar(uri).inject({}) do |cookies, cookie|
|
62
|
+
cookies.merge((uri ? cookie.dispose_for?(uri) : true) ? {cookie.name => cookie} : {})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Cookie
|
67
|
+
include ::Rack::Utils
|
68
|
+
|
69
|
+
attr_reader :raw, :name, :value, :domain, :path, :expires, :default_host
|
70
|
+
|
71
|
+
def initialize raw, uri
|
72
|
+
@default_host = RACK_RADAR__DEFAULT_HOST
|
73
|
+
|
74
|
+
uri ||= default_uri
|
75
|
+
uri.host ||= default_host
|
76
|
+
|
77
|
+
@raw, @options = raw.split(/[;,] */n, 2)
|
78
|
+
@name, @value = parse_query(@raw, ';').to_a.first
|
79
|
+
@options = parse_query(@options, ';')
|
80
|
+
|
81
|
+
@domain = @options['domain'] || uri.host || default_host
|
82
|
+
@domain = '.' << @domain unless @domain =~ /\A\./
|
83
|
+
|
84
|
+
@path = @options['path'] || uri.path.sub(/\/[^\/]*\Z/, '')
|
85
|
+
|
86
|
+
(expires = @options['expires']) && (@expires = ::Time.parse(expires))
|
87
|
+
end
|
88
|
+
|
89
|
+
def replaces? cookie
|
90
|
+
[name.downcase, domain, path] == [cookie.name.downcase, cookie.domain, cookie.path]
|
91
|
+
end
|
92
|
+
|
93
|
+
def empty?
|
94
|
+
value.nil? || value.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
def secure?
|
98
|
+
@options.has_key?('secure')
|
99
|
+
end
|
100
|
+
|
101
|
+
def expired?
|
102
|
+
expires && expires < ::Time.now.gmtime
|
103
|
+
end
|
104
|
+
|
105
|
+
def dispose_for? uri
|
106
|
+
expired? ? false : valid?(uri)
|
107
|
+
end
|
108
|
+
|
109
|
+
def valid? uri = nil
|
110
|
+
uri ||= default_uri
|
111
|
+
uri.host ||= default_host
|
112
|
+
(secure? ? uri.scheme == 'https' : true) &&
|
113
|
+
(uri.host =~ /#{::Regexp.escape(domain.sub(/\A\./, ''))}\Z/i) &&
|
114
|
+
(uri.path =~ /\A#{::Regexp.escape(path)}/)
|
115
|
+
end
|
116
|
+
|
117
|
+
def <=> cookie
|
118
|
+
[name, path, domain.reverse] <=> [cookie.name, cookie.path, cookie.domain.reverse]
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def default_uri
|
123
|
+
::URI.parse('//' << default_host << '/')
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module Rack
|
2
|
+
module Radar
|
3
|
+
class RackRadarSession
|
4
|
+
include ::Rack::Radar
|
5
|
+
|
6
|
+
attr_reader :headers, :cookies, :last_request, :last_response
|
7
|
+
alias header headers
|
8
|
+
|
9
|
+
def initialize app
|
10
|
+
@app = assert_valid_app(app)
|
11
|
+
@headers, @cookies = {}, RackRadarCookies.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def basic_authorize user, pass
|
15
|
+
@basic_auth = [user, pass]
|
16
|
+
end
|
17
|
+
alias authorize basic_authorize
|
18
|
+
alias auth basic_authorize
|
19
|
+
|
20
|
+
def digest_authorize user, pass
|
21
|
+
@digest_auth = [user, pass]
|
22
|
+
end
|
23
|
+
alias digest_auth digest_authorize
|
24
|
+
|
25
|
+
def token_authorize token, options = {}
|
26
|
+
@token_auth = [token, options]
|
27
|
+
end
|
28
|
+
alias token_auth token_authorize
|
29
|
+
|
30
|
+
def reset_basic_auth!
|
31
|
+
@basic_auth = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset_digest_auth!
|
35
|
+
@digest_auth = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset_token_auth!
|
39
|
+
@token_auth = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset_auth!
|
43
|
+
reset_basic_auth!
|
44
|
+
reset_digest_auth!
|
45
|
+
reset_token_auth!
|
46
|
+
end
|
47
|
+
|
48
|
+
def invoke_request scheme, request_method, base_url, path, params, env
|
49
|
+
uri = uri(scheme, base_url, path)
|
50
|
+
|
51
|
+
env = RACK_RADAR__DEFAULT_ENV.merge(env)
|
52
|
+
env.update :method => request_method
|
53
|
+
env.update :params => normalize_params(params)
|
54
|
+
env.update 'HTTP_COOKIE' => cookies.to_s(uri)
|
55
|
+
env.update basic_auth_header
|
56
|
+
env.update token_auth_header
|
57
|
+
|
58
|
+
process_request(@app, uri, env)
|
59
|
+
|
60
|
+
if @digest_auth && @last_response.status == 401 && (challenge = @last_response['WWW-Authenticate'])
|
61
|
+
env.update(digest_auth_header(challenge, uri.path, request_method))
|
62
|
+
process_request(@app, uri, env)
|
63
|
+
end
|
64
|
+
|
65
|
+
cookies.persist(@last_response.header['Set-Cookie'], uri)
|
66
|
+
|
67
|
+
@last_response.respond_to?(:finish) && @last_response.finish
|
68
|
+
@last_response
|
69
|
+
end
|
70
|
+
|
71
|
+
def __r__session
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def normalize_params params
|
78
|
+
params.inject({}) do |params, (k, v)|
|
79
|
+
k = k.to_s if k.is_a?(Numeric) || k.is_a?(Symbol)
|
80
|
+
v = v.to_s if v.is_a?(Numeric) || v.is_a?(Symbol)
|
81
|
+
params.merge k => v
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def assert_valid_app app
|
86
|
+
return app if app.respond_to?(:call)
|
87
|
+
raise(ArgumentError, 'app should be a valid Rack app')
|
88
|
+
end
|
89
|
+
|
90
|
+
def uri scheme, base_url, path
|
91
|
+
path = path*'/'
|
92
|
+
|
93
|
+
# base_url ignored if given path starting with a protocol or a slash
|
94
|
+
path = [base_url, path].compact.map(&:to_s)*'/' unless path =~ %r[\A/|\A(\w+)?://]
|
95
|
+
|
96
|
+
# removing leading slash(es)
|
97
|
+
path.sub!(/\A\/+/, '')
|
98
|
+
|
99
|
+
uri = ::URI.parse(path)
|
100
|
+
# adding leading slash unless path starts with a protocol
|
101
|
+
uri.path = '/' << uri.path unless path =~ %r[\A(\w+)?://]
|
102
|
+
|
103
|
+
uri.host ||= RACK_RADAR__DEFAULT_HOST
|
104
|
+
uri.scheme ||= scheme.to_s
|
105
|
+
|
106
|
+
# now that we have a full URI, eg. with scheme, port etc.
|
107
|
+
# lets build a new one based on it
|
108
|
+
::URI.parse(uri.to_s)
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_request app, path, env
|
112
|
+
env = ::Rack::MockRequest.env_for(path.to_s, Hash[env])
|
113
|
+
explicit_env = headers_to_env
|
114
|
+
explicit_env['rack.input'] && env['REQUEST_METHOD'] == 'POST' && env.delete('CONTENT_TYPE')
|
115
|
+
env.update explicit_env
|
116
|
+
|
117
|
+
@last_request = ::Rack::Request.new(env)
|
118
|
+
|
119
|
+
# initializing params. do not remove! needed for nested params to work
|
120
|
+
@last_request.params
|
121
|
+
|
122
|
+
status, headers, body = app.call(@last_request.env)
|
123
|
+
|
124
|
+
@last_response = ::Rack::MockResponse.new(status, headers, body, env['rack.errors'].flush)
|
125
|
+
body.respond_to?(:close) && body.close
|
126
|
+
end
|
127
|
+
|
128
|
+
def headers_to_env
|
129
|
+
headers.keys.inject({}) do |headers, key|
|
130
|
+
value = self.headers[key]
|
131
|
+
if (key =~ /\A[[:upper:]].*\-?[[:upper:]]?.*?/) && (key !~ /\AHTTP_|\ACONTENT_TYPE\Z/)
|
132
|
+
key = (key == 'Content-Type' ? '' : 'HTTP_') << key.upcase.gsub('-', '_')
|
133
|
+
end
|
134
|
+
headers.merge key => value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def basic_auth_header
|
139
|
+
return {} unless auth = @basic_auth
|
140
|
+
{'HTTP_AUTHORIZATION' => 'Basic %s' % ["#{auth.first}:#{auth.last}"].pack("m*")}
|
141
|
+
end
|
142
|
+
|
143
|
+
def digest_auth_header challenge, uri, request_method
|
144
|
+
params = ::Rack::Auth::Digest::Params.parse(challenge.split(" ", 2).last)
|
145
|
+
params.update({
|
146
|
+
"username" => @digest_auth.first,
|
147
|
+
"nc" => "00000001",
|
148
|
+
"cnonce" => "nonsensenonce",
|
149
|
+
"uri" => uri,
|
150
|
+
"method" => request_method,
|
151
|
+
})
|
152
|
+
params["response"] = MockDigestRequest.new(params).response(@digest_auth.last)
|
153
|
+
{'HTTP_AUTHORIZATION' => 'Digest ' << params.map {|p| '%s="%s"' % p}.join(', ')}
|
154
|
+
end
|
155
|
+
|
156
|
+
def token_auth_header
|
157
|
+
return {} unless auth = @token_auth
|
158
|
+
chunks = auth[1].each_with_object(['token=%s' % auth[0].to_s.inspect]) do |(k,v),o|
|
159
|
+
o << [key, value.to_s.inspect]*'='
|
160
|
+
end
|
161
|
+
{'HTTP_AUTHORIZATION' => 'Token %s' % chunks.join(', ')}
|
162
|
+
end
|
163
|
+
|
164
|
+
class MockDigestRequest # stolen from rack-test
|
165
|
+
|
166
|
+
def initialize(params)
|
167
|
+
@params = params
|
168
|
+
end
|
169
|
+
|
170
|
+
def method_missing(sym)
|
171
|
+
if @params.has_key? k = sym.to_s
|
172
|
+
return @params[k]
|
173
|
+
end
|
174
|
+
|
175
|
+
super
|
176
|
+
end
|
177
|
+
|
178
|
+
def method
|
179
|
+
@params['method']
|
180
|
+
end
|
181
|
+
|
182
|
+
def response(password)
|
183
|
+
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/rack-radar.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
name, version = 'rack-radar 0.0.1'.split
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = name
|
6
|
+
spec.version = version
|
7
|
+
spec.authors = ['Slee Woo']
|
8
|
+
spec.email = ['mail@sleewoo.com']
|
9
|
+
spec.description = 'A simple API for testing Rack applications via Rack::MockRequest'
|
10
|
+
spec.summary = [name, version]*'-'
|
11
|
+
spec.homepage = 'https://github.com/sleewoo/' + name
|
12
|
+
spec.license = 'MIT'
|
13
|
+
|
14
|
+
spec.files = Dir['**/{*,.[a-z]*}'].reject {|e| e =~ /\.(gem|lock)\Z/}
|
15
|
+
spec.require_paths = ['lib']
|
16
|
+
|
17
|
+
spec.required_ruby_version = '>= 1.9.2'
|
18
|
+
spec.add_development_dependency 'minispec'
|
19
|
+
spec.add_development_dependency 'bundler'
|
20
|
+
spec.add_development_dependency 'rake'
|
21
|
+
end
|
data/test/app_test.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test_setup'
|
2
|
+
|
3
|
+
class AppTest
|
4
|
+
include Minispec
|
5
|
+
|
6
|
+
class App < Air
|
7
|
+
def index
|
8
|
+
__method__
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class JustAnotherApp < Air
|
13
|
+
def another
|
14
|
+
__method__
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
before_all { app(App) }
|
19
|
+
|
20
|
+
testing :response do
|
21
|
+
get
|
22
|
+
is(last_response).ok?
|
23
|
+
assert(last_response.body) == 'index'
|
24
|
+
end
|
25
|
+
|
26
|
+
testing :cookies do
|
27
|
+
cookies['foo'] = 'bar'
|
28
|
+
get
|
29
|
+
assert(cookies['foo']) == 'bar'
|
30
|
+
end
|
31
|
+
|
32
|
+
testing :headers do
|
33
|
+
header['User-Agent'] = 'Rack::Radar'
|
34
|
+
get
|
35
|
+
assert(headers['User-Agent']) == 'Rack::Radar'
|
36
|
+
end
|
37
|
+
|
38
|
+
testing 'another app' do
|
39
|
+
|
40
|
+
app(JustAnotherApp)
|
41
|
+
|
42
|
+
are(cookies['foo']).nil?
|
43
|
+
are(headers['User-Agent']).nil?
|
44
|
+
|
45
|
+
get
|
46
|
+
is(last_response).not_found?
|
47
|
+
|
48
|
+
get :another
|
49
|
+
is(last_response).ok?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/test/auth_test.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'test_setup'
|
2
|
+
|
3
|
+
class AuthTest
|
4
|
+
include Minispec
|
5
|
+
|
6
|
+
class BasicAuth < Air
|
7
|
+
|
8
|
+
use Rack::Auth::Basic do |u, p|
|
9
|
+
[u, p] == ['user', 'pass']
|
10
|
+
end
|
11
|
+
|
12
|
+
def index
|
13
|
+
__method__
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_index
|
17
|
+
__method__
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class DigestAuth < Air
|
22
|
+
|
23
|
+
use Rack::Auth::Digest::MD5, 'AccessRestricted', rand.to_s do |u|
|
24
|
+
{'digest-user' => 'digest-pass'}[u]
|
25
|
+
end
|
26
|
+
|
27
|
+
def index
|
28
|
+
__method__
|
29
|
+
end
|
30
|
+
|
31
|
+
def post_index
|
32
|
+
__method__
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
helper :authenticated? do |_,body|
|
37
|
+
is(last_response).ok?
|
38
|
+
assert(last_response.body) == (body || 'index')
|
39
|
+
end
|
40
|
+
|
41
|
+
helper :restricted? do
|
42
|
+
assert(last_response.status) == 401
|
43
|
+
end
|
44
|
+
|
45
|
+
context :Basic do
|
46
|
+
before_all { app BasicAuth }
|
47
|
+
|
48
|
+
it 'returns 401 when unauthorized' do
|
49
|
+
get
|
50
|
+
is.restricted?
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'authenticate and return 200' do
|
54
|
+
auth 'user', 'pass'
|
55
|
+
get
|
56
|
+
is.authenticated?
|
57
|
+
end
|
58
|
+
|
59
|
+
testing 'reset_auth!' do
|
60
|
+
reset_auth!
|
61
|
+
get
|
62
|
+
is.restricted?
|
63
|
+
end
|
64
|
+
|
65
|
+
should 'relogin after reset_auth! used' do
|
66
|
+
auth 'user', 'pass'
|
67
|
+
get
|
68
|
+
is.authenticated?
|
69
|
+
end
|
70
|
+
|
71
|
+
testing 'reset_basic_auth!' do
|
72
|
+
reset_basic_auth!
|
73
|
+
get
|
74
|
+
is.restricted?
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'relogin after reset_basic_auth! used' do
|
78
|
+
auth 'user', 'pass'
|
79
|
+
get
|
80
|
+
is.authenticated?
|
81
|
+
end
|
82
|
+
|
83
|
+
should 'fail with wrong credentials' do
|
84
|
+
reset_basic_auth!
|
85
|
+
auth 'bad', 'guy'
|
86
|
+
get
|
87
|
+
is.restricted?
|
88
|
+
end
|
89
|
+
|
90
|
+
should 'auth via POST' do
|
91
|
+
reset_basic_auth!
|
92
|
+
auth 'user', 'pass'
|
93
|
+
post
|
94
|
+
is.authenticated? 'post_index'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context :Digest do
|
99
|
+
before_all { app DigestAuth }
|
100
|
+
|
101
|
+
it 'is restricted by default' do
|
102
|
+
get
|
103
|
+
is.restricted?
|
104
|
+
end
|
105
|
+
|
106
|
+
should 'authenticate and return 200' do
|
107
|
+
digest_auth 'digest-user', 'digest-pass'
|
108
|
+
get
|
109
|
+
is.authenticated?
|
110
|
+
end
|
111
|
+
|
112
|
+
should 'reset auth using reset_auth!' do
|
113
|
+
reset_auth!
|
114
|
+
get
|
115
|
+
is.restricted?
|
116
|
+
end
|
117
|
+
|
118
|
+
should 'relogin after reset_auth! used' do
|
119
|
+
digest_auth 'digest-user', 'digest-pass'
|
120
|
+
get
|
121
|
+
is.authenticated?
|
122
|
+
end
|
123
|
+
|
124
|
+
should 'reset auth using reset_digest_auth!' do
|
125
|
+
reset_digest_auth!
|
126
|
+
get
|
127
|
+
is.restricted?
|
128
|
+
end
|
129
|
+
|
130
|
+
should 'relogin after reset_digest_auth! used' do
|
131
|
+
digest_auth 'digest-user', 'digest-pass'
|
132
|
+
get
|
133
|
+
is.authenticated?
|
134
|
+
end
|
135
|
+
|
136
|
+
should 'fail with wrong credentials' do
|
137
|
+
reset_digest_auth!
|
138
|
+
digest_auth 'bad', 'guy'
|
139
|
+
get
|
140
|
+
is.restricted?
|
141
|
+
end
|
142
|
+
|
143
|
+
should 'auth via POST' do
|
144
|
+
reset_digest_auth!
|
145
|
+
digest_auth 'digest-user', 'digest-pass'
|
146
|
+
post
|
147
|
+
is.authenticated? 'post_index'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|