sonar 0.2.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.
- 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
|