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.
@@ -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