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