rack-radar 0.0.1
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 +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
|