rack-radar 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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