sonar 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/LICENSE +19 -0
- data/README.md +283 -0
- data/Rakefile +27 -0
- data/lib/sonar.rb +97 -0
- data/lib/sonar/cookies.rb +125 -0
- data/lib/sonar/session.rb +150 -0
- data/sonar.gemspec +25 -0
- data/test/setup.rb +152 -0
- data/test/test__app.rb +71 -0
- data/test/test__auth.rb +144 -0
- data/test/test__cookies.rb +284 -0
- data/test/test__follow_redirect.rb +40 -0
- data/test/test__headers.rb +94 -0
- data/test/test__map.rb +73 -0
- data/test/test__params.rb +47 -0
- data/test/test__request_methods.rb +76 -0
- data/test/test__session.rb +49 -0
- metadata +131 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
class SonarSession
|
2
|
+
|
3
|
+
include ::Sonar
|
4
|
+
|
5
|
+
attr_reader :headers, :cookies, :last_request, :last_response
|
6
|
+
alias header headers
|
7
|
+
|
8
|
+
def initialize app
|
9
|
+
app.respond_to?(:call) ||
|
10
|
+
raise('app should be a valid Rack app')
|
11
|
+
|
12
|
+
@app = app
|
13
|
+
@headers, @cookies = {}, ::SonarCookies.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def basic_authorize user, pass
|
17
|
+
@basic_auth = [user, pass]
|
18
|
+
end
|
19
|
+
|
20
|
+
alias authorize basic_authorize
|
21
|
+
alias auth basic_authorize
|
22
|
+
|
23
|
+
def digest_authorize user, pass
|
24
|
+
@digest_auth = [user, pass]
|
25
|
+
end
|
26
|
+
|
27
|
+
alias digest_auth digest_authorize
|
28
|
+
|
29
|
+
def reset_basic_auth!
|
30
|
+
@basic_auth = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset_digest_auth!
|
34
|
+
@digest_auth = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset_auth!
|
38
|
+
reset_basic_auth!
|
39
|
+
reset_digest_auth!
|
40
|
+
end
|
41
|
+
|
42
|
+
def invoke_request request_method, uri, params, env
|
43
|
+
|
44
|
+
default_env = ::SonarConstants::DEFAULT_ENV.dup.merge(env)
|
45
|
+
default_env.update :method => request_method
|
46
|
+
default_env.update :params => params
|
47
|
+
default_env.update 'HTTP_COOKIE' => cookies.to_s(uri)
|
48
|
+
default_env.update basic_auth_header
|
49
|
+
|
50
|
+
process_request uri, default_env
|
51
|
+
|
52
|
+
if @digest_auth && @last_response.status == 401 && (challenge = @last_response['WWW-Authenticate'])
|
53
|
+
default_env.update digest_auth_header(challenge, uri.path, request_method)
|
54
|
+
process_request uri, default_env
|
55
|
+
end
|
56
|
+
|
57
|
+
cookies.persist(@last_response.header['Set-Cookie'], uri)
|
58
|
+
|
59
|
+
@last_response.respond_to?(:finish) && @last_response.finish
|
60
|
+
@last_response
|
61
|
+
end
|
62
|
+
|
63
|
+
def __sonar__session__
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset_app!
|
68
|
+
raise 'It makes no sense to use `%s` with manually created sessions. To test another app, just create a new session.' % __method__
|
69
|
+
end
|
70
|
+
|
71
|
+
alias reset_browser! reset_app!
|
72
|
+
|
73
|
+
def app *args
|
74
|
+
args.any? && raise('It makes no sense to use `%s` with manually created sessions. To test another app, just create a new session.' % __method__)
|
75
|
+
@app
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def process_request uri, env
|
80
|
+
env = ::Rack::MockRequest.env_for(uri.to_s, env.dup)
|
81
|
+
explicit_env = headers_to_env
|
82
|
+
explicit_env['rack.input'] &&
|
83
|
+
env['REQUEST_METHOD'] == 'POST' && env.delete('CONTENT_TYPE')
|
84
|
+
env.update explicit_env
|
85
|
+
|
86
|
+
@last_request = ::Rack::Request.new(env)
|
87
|
+
|
88
|
+
# initializing params. do not remove! needed for nested params to work
|
89
|
+
@last_request.params
|
90
|
+
|
91
|
+
status, headers, body = app.call(@last_request.env)
|
92
|
+
|
93
|
+
@last_response = ::Rack::MockResponse.new(status, headers, body, env['rack.errors'].flush)
|
94
|
+
body.respond_to?(:close) && body.close
|
95
|
+
end
|
96
|
+
|
97
|
+
def headers_to_env
|
98
|
+
headers.keys.inject({}) do |headers, key|
|
99
|
+
value = headers()[key]
|
100
|
+
if (key =~ /\A[[:upper:]].*\-?[[:upper:]]?.*?/) && (key !~ /\AHTTP_|\ACONTENT_TYPE\Z/)
|
101
|
+
key = (key == 'Content-Type' ? '' : 'HTTP_') << key.upcase.gsub('-', '_')
|
102
|
+
end
|
103
|
+
headers.merge key => value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def basic_auth_header
|
108
|
+
(auth = @basic_auth) ?
|
109
|
+
{'HTTP_AUTHORIZATION' => 'Basic %s' % ["#{auth.first}:#{auth.last}"].pack("m*")} :
|
110
|
+
{}
|
111
|
+
end
|
112
|
+
|
113
|
+
def digest_auth_header challenge, uri, request_method
|
114
|
+
params = ::Rack::Auth::Digest::Params.parse(challenge.split(" ", 2).last)
|
115
|
+
params.merge!({
|
116
|
+
"username" => @digest_auth.first,
|
117
|
+
"nc" => "00000001",
|
118
|
+
"cnonce" => "nonsensenonce",
|
119
|
+
"uri" => uri,
|
120
|
+
"method" => request_method,
|
121
|
+
})
|
122
|
+
params["response"] = MockDigestRequest.new(params).response(@digest_auth.last)
|
123
|
+
{'HTTP_AUTHORIZATION' => 'Digest ' << params.map {|p| '%s="%s"' % p}.join(', ')}
|
124
|
+
end
|
125
|
+
|
126
|
+
class MockDigestRequest # stolen from rack-test
|
127
|
+
|
128
|
+
def initialize(params)
|
129
|
+
@params = params
|
130
|
+
end
|
131
|
+
|
132
|
+
def method_missing(sym)
|
133
|
+
if @params.has_key? k = sym.to_s
|
134
|
+
return @params[k]
|
135
|
+
end
|
136
|
+
|
137
|
+
super
|
138
|
+
end
|
139
|
+
|
140
|
+
def method
|
141
|
+
@params['method']
|
142
|
+
end
|
143
|
+
|
144
|
+
def response(password)
|
145
|
+
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
data/sonar.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
version = '0.2.1'
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
|
6
|
+
s.name = 'sonar'
|
7
|
+
s.version = version
|
8
|
+
s.authors = ['Walter Smith', 'Joran Kikke']
|
9
|
+
s.email = ['waltee.smith@gmail.com', 'joran.k@gmail.com']
|
10
|
+
s.homepage = 'https://github.com/dangerousbeans/sonar'
|
11
|
+
s.summary = 'sonar-%s' % version
|
12
|
+
s.description = 'API for Testing Rack Applications via Mock HTTP'
|
13
|
+
|
14
|
+
s.required_ruby_version = '>= 1.9.2'
|
15
|
+
|
16
|
+
s.add_dependency 'rack', '~> 1.5'
|
17
|
+
|
18
|
+
s.add_development_dependency 'rake', '~> 10'
|
19
|
+
s.add_development_dependency 'specular', '~> 0.2.2'
|
20
|
+
s.add_development_dependency 'bundler'
|
21
|
+
|
22
|
+
s.require_paths = ['lib']
|
23
|
+
s.files = Dir['**/{*,.[a-z]*}'].reject {|e| e =~ /\.(gem|lock)\Z/}
|
24
|
+
s.licenses = ['MIT']
|
25
|
+
end
|
data/test/setup.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'specular'
|
3
|
+
|
4
|
+
$:.unshift ::File.expand_path '../../lib', __FILE__
|
5
|
+
require 'sonar'
|
6
|
+
|
7
|
+
class Air < ::Rack::Request
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def use ware = nil, *args, &proc
|
11
|
+
@middleware ||= []
|
12
|
+
@middleware << [ware, args, proc] if ware
|
13
|
+
@middleware
|
14
|
+
end
|
15
|
+
|
16
|
+
def map
|
17
|
+
@map
|
18
|
+
end
|
19
|
+
|
20
|
+
def action_map
|
21
|
+
@action_map
|
22
|
+
end
|
23
|
+
|
24
|
+
def app root = nil
|
25
|
+
if root
|
26
|
+
@root = root
|
27
|
+
else
|
28
|
+
return @app if @app
|
29
|
+
end
|
30
|
+
map!
|
31
|
+
|
32
|
+
builder, app = ::Rack::Builder.new, self
|
33
|
+
use.each do |w|
|
34
|
+
ware, args, proc = w
|
35
|
+
builder.use ware, *args, &proc
|
36
|
+
end
|
37
|
+
map.each_key do |route|
|
38
|
+
builder.map route do
|
39
|
+
run lambda { |env| app.new(env).__air__response__ route }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@app = builder.to_app
|
43
|
+
end
|
44
|
+
|
45
|
+
alias to_app app
|
46
|
+
|
47
|
+
def call env
|
48
|
+
app.call env
|
49
|
+
end
|
50
|
+
|
51
|
+
def base_url
|
52
|
+
@root || '/'
|
53
|
+
end
|
54
|
+
|
55
|
+
alias baseurl base_url
|
56
|
+
|
57
|
+
private
|
58
|
+
def map!
|
59
|
+
@action_map = {}
|
60
|
+
@map = self.instance_methods(false).reject { |m| m.to_s =~ /^__air__/ }.inject({}) do |map, meth|
|
61
|
+
route, request_method = meth.to_s, 'GET'
|
62
|
+
SonarConstants::REQUEST_METHODS.each do |rm|
|
63
|
+
regex = /^#{rm}_/i
|
64
|
+
route =~ regex && (route = route.sub(regex, '')) && (request_method = rm.upcase) && break
|
65
|
+
end
|
66
|
+
|
67
|
+
{'____' => '.',
|
68
|
+
'___' => '-',
|
69
|
+
'__' => '/'}.each_pair { |f, t| route = route.gsub(f, t) }
|
70
|
+
|
71
|
+
arity = self.instance_method(meth).arity
|
72
|
+
setup = [meth, arity < 0 ? -arity - 1 : arity]
|
73
|
+
(map[rootify(route)] ||={})[request_method] = setup
|
74
|
+
(map[rootify] ||= {})[request_method] = setup if route == 'index'
|
75
|
+
@action_map[meth.to_sym] = rootify(route)
|
76
|
+
map
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def rootify route = nil
|
81
|
+
('/%s/%s' % [base_url, route]).gsub /\/+/, '/'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :response
|
86
|
+
|
87
|
+
def __air__response__ route
|
88
|
+
rsp = catch :__air__halt__ do
|
89
|
+
@response = ::Rack::Response.new
|
90
|
+
rest_map = self.class.map[route] || halt(404)
|
91
|
+
action, required_parameters = rest_map[env['REQUEST_METHOD']] || halt(404)
|
92
|
+
arguments = env['PATH_INFO'].to_s.split('/').select { |c| c.size > 0 }
|
93
|
+
arguments.size == required_parameters || halt(404, '%s arguments expected, %s given' % [required_parameters, arguments.size])
|
94
|
+
response.body = [self.send(action, *arguments).to_s]
|
95
|
+
response
|
96
|
+
end
|
97
|
+
rsp['Content-Type'] ||= 'text/html'
|
98
|
+
rsp.finish
|
99
|
+
end
|
100
|
+
|
101
|
+
def base_url
|
102
|
+
self.class.base_url
|
103
|
+
end
|
104
|
+
|
105
|
+
def halt status, message = nil
|
106
|
+
response.status = status
|
107
|
+
response.body = [message] if message
|
108
|
+
throw :__air__halt__, response
|
109
|
+
end
|
110
|
+
|
111
|
+
def redirect action_or_path
|
112
|
+
response['Location'] = self.class.action_map[action_or_path] || action_or_path
|
113
|
+
response.status = 302
|
114
|
+
throw :__air__halt__, response
|
115
|
+
end
|
116
|
+
|
117
|
+
def permanent_redirect action_or_path
|
118
|
+
response['Location'] = self.class.action_map[action_or_path] || action_or_path
|
119
|
+
response.status = 301
|
120
|
+
throw :__air__halt__, response
|
121
|
+
end
|
122
|
+
|
123
|
+
def params
|
124
|
+
@__air__params__ ||= indifferent_params(super)
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_params
|
128
|
+
@__air__get_params__ ||= indifferent_params(self.GET)
|
129
|
+
end
|
130
|
+
|
131
|
+
def post_params
|
132
|
+
@__air__post_params__ ||= indifferent_params(self.POST)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def indifferent_params(object)
|
137
|
+
case object
|
138
|
+
when Hash
|
139
|
+
new_hash = indifferent_hash
|
140
|
+
object.each { |key, value| new_hash[key] = indifferent_params(value) }
|
141
|
+
new_hash
|
142
|
+
when Array
|
143
|
+
object.map { |item| indifferent_params(item) }
|
144
|
+
else
|
145
|
+
object
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def indifferent_hash
|
150
|
+
Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
|
151
|
+
end
|
152
|
+
end
|
data/test/test__app.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module SonarTest__app
|
2
|
+
|
3
|
+
class App < Air
|
4
|
+
|
5
|
+
def index
|
6
|
+
__method__
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
class JustAnotherApp < Air
|
12
|
+
def another
|
13
|
+
__method__
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Spec.new App do
|
18
|
+
|
19
|
+
get
|
20
|
+
expect(last_response.status) == 200
|
21
|
+
expect(last_response.body) == 'index'
|
22
|
+
|
23
|
+
Testing :cookies do
|
24
|
+
cookies['foo'] = 'bar'
|
25
|
+
expect(cookies['foo']) == 'bar'
|
26
|
+
end
|
27
|
+
|
28
|
+
Testing :headers do
|
29
|
+
header['User-Agent'] = 'Sonar'
|
30
|
+
expect(headers['User-Agent']) == 'Sonar'
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
When 'switching app' do
|
35
|
+
app JustAnotherApp
|
36
|
+
|
37
|
+
It 'should use own cookies jar' do
|
38
|
+
is(cookies['foo']).nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
And 'own headers' do
|
42
|
+
is(headers['User-Agent']).nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
get
|
46
|
+
expect(last_response.status) == 404
|
47
|
+
|
48
|
+
get :another
|
49
|
+
expect(last_response.status) == 200
|
50
|
+
end
|
51
|
+
|
52
|
+
When 'switching back to default app' do
|
53
|
+
app App
|
54
|
+
|
55
|
+
It 'should have previously cookies set' do
|
56
|
+
expect(cookies['foo']) == 'bar'
|
57
|
+
end
|
58
|
+
|
59
|
+
And :headers do
|
60
|
+
expect(headers['User-Agent']) == 'Sonar'
|
61
|
+
end
|
62
|
+
|
63
|
+
get
|
64
|
+
expect(last_response.status) == 200
|
65
|
+
|
66
|
+
get :another
|
67
|
+
expect(last_response.status) == 404
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
data/test/test__auth.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
module SonarTest__auth
|
2
|
+
|
3
|
+
class Basic < Air
|
4
|
+
|
5
|
+
use Rack::Auth::Basic do |u, p|
|
6
|
+
[u, p] == ['user', 'pass']
|
7
|
+
end
|
8
|
+
|
9
|
+
def index
|
10
|
+
__method__
|
11
|
+
end
|
12
|
+
|
13
|
+
def post_index
|
14
|
+
__method__
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class Digest < Air
|
20
|
+
|
21
|
+
use Rack::Auth::Digest::MD5, 'AccessRestricted', rand.to_s do |u|
|
22
|
+
{'digest-user' => 'digest-pass'}[u]
|
23
|
+
end
|
24
|
+
|
25
|
+
def index
|
26
|
+
__method__
|
27
|
+
end
|
28
|
+
|
29
|
+
def post_index
|
30
|
+
__method__
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
Spec.new self do
|
36
|
+
|
37
|
+
Testing :Basic do
|
38
|
+
app Basic
|
39
|
+
|
40
|
+
r = get
|
41
|
+
expect(r.status) == 401
|
42
|
+
|
43
|
+
auth 'user', 'pass'
|
44
|
+
r = get
|
45
|
+
expect(r.status) == 200
|
46
|
+
expect(r.body) == 'index'
|
47
|
+
|
48
|
+
o 'reset auth using `reset_auth!`'
|
49
|
+
reset_auth!
|
50
|
+
|
51
|
+
r = get
|
52
|
+
expect(r.status) == 401
|
53
|
+
|
54
|
+
o 'relogin'
|
55
|
+
auth 'user', 'pass'
|
56
|
+
r = get
|
57
|
+
expect(r.status) == 200
|
58
|
+
expect(r.body) == 'index'
|
59
|
+
|
60
|
+
o 'reset auth using `reset_basic_auth!`'
|
61
|
+
reset_basic_auth!
|
62
|
+
|
63
|
+
r = get
|
64
|
+
expect(r.status) == 401
|
65
|
+
|
66
|
+
o 'relogin'
|
67
|
+
auth 'user', 'pass'
|
68
|
+
r = get
|
69
|
+
expect(r.status) == 200
|
70
|
+
expect(r.body) == 'index'
|
71
|
+
|
72
|
+
Should 'fail with wrong credentials' do
|
73
|
+
reset_basic_auth!
|
74
|
+
|
75
|
+
auth 'bad', 'guy'
|
76
|
+
r = get
|
77
|
+
expect(r.status) == 401
|
78
|
+
end
|
79
|
+
|
80
|
+
Should 'auth via POST' do
|
81
|
+
reset_basic_auth!
|
82
|
+
|
83
|
+
auth 'user', 'pass'
|
84
|
+
r = post
|
85
|
+
expect(r.status) == 200
|
86
|
+
expect(r.body) == 'post_index'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
Testing :Digest do
|
91
|
+
app Digest
|
92
|
+
|
93
|
+
r = get
|
94
|
+
expect(r.status) == 401
|
95
|
+
|
96
|
+
digest_auth 'digest-user', 'digest-pass'
|
97
|
+
r = get
|
98
|
+
expect(r.status) == 200
|
99
|
+
expect(r.body) == 'index'
|
100
|
+
|
101
|
+
o 'reset auth using `reset_auth!`'
|
102
|
+
reset_digest_auth!
|
103
|
+
|
104
|
+
r = get
|
105
|
+
expect(r.status) == 401
|
106
|
+
|
107
|
+
o 'relogin'
|
108
|
+
digest_auth 'digest-user', 'digest-pass'
|
109
|
+
r = get
|
110
|
+
expect(r.status) == 200
|
111
|
+
expect(r.body) == 'index'
|
112
|
+
|
113
|
+
o 'reset auth using `reset_basic_auth!`'
|
114
|
+
reset_digest_auth!
|
115
|
+
|
116
|
+
r = get
|
117
|
+
expect(r.status) == 401
|
118
|
+
|
119
|
+
o 'relogin'
|
120
|
+
digest_auth 'digest-user', 'digest-pass'
|
121
|
+
r = get
|
122
|
+
expect(r.status) == 200
|
123
|
+
expect(r.body) == 'index'
|
124
|
+
|
125
|
+
Should 'fail with wrong credentials' do
|
126
|
+
reset_digest_auth!
|
127
|
+
|
128
|
+
digest_auth 'bad', 'guy'
|
129
|
+
r = get
|
130
|
+
expect(r.status) == 401
|
131
|
+
end
|
132
|
+
|
133
|
+
Should 'auth via POST' do
|
134
|
+
reset_digest_auth!
|
135
|
+
|
136
|
+
digest_auth 'digest-user', 'digest-pass'
|
137
|
+
r = post
|
138
|
+
expect(r.status) == 200
|
139
|
+
expect(r.body) == 'post_index'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|