mosquito 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +22 -0
- data/Manifest.txt +10 -16
- data/README.txt +54 -29
- data/Rakefile +29 -2
- data/lib/mosquito.rb +444 -87
- data/public/bare.rb +71 -0
- data/public/blog.rb +1 -6
- data/public/blog/controllers.rb +157 -110
- data/public/blog/models.rb +8 -9
- data/public/blog/views.rb +59 -61
- data/test/sage_advice_cases/parsing_arrays.rb +25 -0
- data/test/test_bare.rb +66 -0
- data/test/test_blog.rb +140 -26
- data/test/test_helpers.rb +20 -0
- data/test/test_mock_request.rb +292 -0
- data/test/test_mock_upload.rb +55 -0
- metadata +31 -18
- data/public/homepage.rb +0 -64
- data/test/test_homepage.rb +0 -23
data/CHANGELOG
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
== 0.1.3 - The little fairy release
|
2
|
+
|
3
|
+
* You can use an absolute URL with scheme and all instead of path-only abbreviation
|
4
|
+
* You can now assign a URL-encoded payload instead of a hash when doing all requests
|
5
|
+
except of a GET. This to be nice to people building web-service backends.
|
6
|
+
* Be nice to the folks that do not use the database or sessions
|
7
|
+
* we have @assigns to access the instance variables summoned in the controller
|
8
|
+
* follow_redirect makes use of the feature below, accordingly
|
9
|
+
* You can now pass verbatim query string parameters like so
|
10
|
+
get "/blog/archive?page=2"
|
11
|
+
which will be conveniently mixed with other params (and can also be used when doing POSTs!)
|
12
|
+
* Rdoc is extremely unfriendly to pluses and stars in Unicode mode. They should be punished.
|
13
|
+
* FunctionalTest is now WebTest and UnitTest is now ModelTest, because the ruby sadists said they shall be.
|
14
|
+
* We now support proper, infinitely nested and encapsulated parameters
|
15
|
+
* for querystrings
|
16
|
+
* for postvars
|
17
|
+
* and yes, for uploads too
|
18
|
+
* On that note, added a Mosquito::MockUpload to quickly simulate an uploaded file. The file will be filled with random text, so roll your own if you need concrete file content.
|
19
|
+
* We are Camping 1.5 compatible
|
20
|
+
* You can now do 'test "should do this"' and pass a block of assertions.
|
21
|
+
* More tests for better coverage of mosquito.rb
|
22
|
+
* Cleanup of Rakefile with other options and proper CHANGELOG inclusion:CHANGELOG
|
1
23
|
|
2
24
|
== 0.1.2
|
3
25
|
|
data/Manifest.txt
CHANGED
@@ -1,26 +1,20 @@
|
|
1
|
-
Rakefile
|
2
|
-
README.txt
|
3
|
-
MIT-LICENSE
|
4
1
|
CHANGELOG
|
2
|
+
MIT-LICENSE
|
5
3
|
Manifest.txt
|
6
|
-
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
7
6
|
lib/mosquito.rb
|
8
|
-
public
|
9
|
-
|
10
|
-
public/blog
|
7
|
+
public/bare.rb
|
11
8
|
public/blog.rb
|
12
9
|
public/blog/controllers.rb
|
13
10
|
public/blog/models.rb
|
14
11
|
public/blog/views.rb
|
15
|
-
|
16
|
-
public/homepage.rb
|
17
|
-
|
18
|
-
test
|
19
|
-
test/test_blog.rb
|
20
|
-
|
21
|
-
test/test_homepage.rb
|
22
|
-
|
23
|
-
test/fixtures
|
24
12
|
test/fixtures/blog_comments.yml
|
25
13
|
test/fixtures/blog_posts.yml
|
26
14
|
test/fixtures/blog_users.yml
|
15
|
+
test/sage_advice_cases/parsing_arrays.rb
|
16
|
+
test/test_bare.rb
|
17
|
+
test/test_blog.rb
|
18
|
+
test/test_helpers.rb
|
19
|
+
test/test_mock_request.rb
|
20
|
+
test/test_mock_upload.rb
|
data/README.txt
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
Mosquito, for Bug-Free Camping
|
2
|
-
=============================
|
1
|
+
=Mosquito, for Bug-Free Camping
|
3
2
|
|
4
3
|
A testing helper for those times when you go Camping.
|
5
|
-
|
6
|
-
|
4
|
+
Apply on the face, neck, and any exposed areas such as your
|
5
|
+
Models and Controllers. Scrub gently, observe the results.
|
7
6
|
|
8
7
|
== Usage
|
9
8
|
|
@@ -18,51 +17,58 @@ Make a few files and directories like this:
|
|
18
17
|
blog_posts.yml
|
19
18
|
blog_users.yml
|
20
19
|
|
21
|
-
Setup
|
20
|
+
Setup <b>test_blog.rb</b> like this:
|
22
21
|
|
23
22
|
require 'rubygems'
|
24
23
|
require 'mosquito'
|
25
24
|
require File.dirname(__FILE__) + "/../public/blog"
|
26
|
-
|
25
|
+
|
27
26
|
Blog.create
|
28
27
|
include Blog::Models
|
29
|
-
|
30
|
-
class TestBlog < Camping::FunctionalTest
|
31
|
-
|
32
|
-
fixtures :blog_posts, :blog_users, :blog_comments
|
33
28
|
|
29
|
+
class TestBlog < Camping::WebTest
|
30
|
+
|
31
|
+
fixtures :blog_posts, :blog_users, :blog_comments
|
32
|
+
|
34
33
|
def setup
|
35
34
|
super
|
36
35
|
# Do other stuff here
|
37
36
|
end
|
38
|
-
|
39
|
-
|
37
|
+
|
38
|
+
test "should get index" do
|
40
39
|
get
|
41
40
|
assert_response :success
|
42
41
|
assert_match_body %r!>blog<!
|
43
42
|
end
|
44
|
-
|
45
|
-
|
43
|
+
|
44
|
+
test "should get view" do
|
46
45
|
get '/view/1'
|
47
46
|
assert_response :success
|
47
|
+
assert_kind_of Article, @assigns[:article]
|
48
48
|
assert_match_body %r!The quick fox jumped over the lazy dog!
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
|
+
test "should change profile" do
|
52
|
+
@request['SERVER_NAME'] = 'jonh.blogs.net'
|
53
|
+
post '/change-profile', :new_photo => upload("picture.jpg")
|
54
|
+
assert_response :success
|
55
|
+
assert_match_body %r!The pic has been uploaded!
|
56
|
+
end
|
51
57
|
end
|
52
58
|
|
53
59
|
# A unit test
|
54
|
-
class TestPost < Camping::
|
60
|
+
class TestPost < Camping::ModelTest
|
55
61
|
|
56
62
|
fixtures :blog_posts, :blog_users, :blog_comments
|
57
63
|
|
58
|
-
|
64
|
+
test "should create" do
|
59
65
|
post = Post.create( :user_id => 1,
|
60
66
|
:title => "Title",
|
61
67
|
:body => "Body")
|
62
68
|
assert post.valid?
|
63
69
|
end
|
64
70
|
|
65
|
-
|
71
|
+
test "should be associated with User" do
|
66
72
|
post = Post.find :first
|
67
73
|
assert_kind_of User, post.user
|
68
74
|
assert_equal 1, post.user.id
|
@@ -70,35 +76,54 @@ Setup +test_blog.rb+ like this:
|
|
70
76
|
|
71
77
|
end
|
72
78
|
|
79
|
+
You can also use old-school methods like <tt>def test_create</tt>, but we think this way is much more natural.
|
80
|
+
|
81
|
+
Mosquito includes Jay Fields' <tt>dust</tt> gem for the nice <tt>test</tt> method which allows more descriptive test names and has the added benefit of detecting those times when you try to write two tests with the same name. Ruby will otherwise silently overwrite duplicate test names without warning, which can give a false sense of security.
|
73
82
|
|
74
83
|
== Details
|
75
84
|
|
76
|
-
Inherit from
|
85
|
+
Inherit from Camping::WebTest or Camping::ModelTest. If you define <tt>setup</tt>,
|
86
|
+
be sure to call <tt>super</tt> so the parent class can do its thing.
|
77
87
|
|
78
|
-
You should also call the
|
88
|
+
You should also call the <tt>MyApp.create</tt> method if you have one, <b>yourself</b>. You will also
|
89
|
+
need to <tt>include MyApp::Models</tt> at the top of your test file if you want to use
|
90
|
+
Models in your assertions directly (without going through MyApp::Models::SomeModel).
|
79
91
|
|
80
|
-
Make fixtures in
|
92
|
+
Make fixtures in <b>test/fixtures</b>. Remember that Camping models use the name of
|
93
|
+
the mount plus the model name: <b>blog_posts</b> for the <b>Post</b> model.
|
81
94
|
|
82
|
-
See
|
95
|
+
See <b>blog_test.rb</b> for an example of both Web and Model tests.
|
83
96
|
|
84
|
-
|
97
|
+
Mosquito is one file, just like your app (right?), so feel free to ship it included with the app itself
|
98
|
+
to simplify testing.
|
85
99
|
|
86
|
-
|
100
|
+
== Warning: You are Camping, not Rail-riding
|
87
101
|
|
88
|
-
|
102
|
+
These directives are highly recommended when using Mosquito:
|
89
103
|
|
90
|
-
|
104
|
+
* Test files start with <b>test_</b> (test_blog.rb). Test classes start with <b>Test</b> (TestBlog).
|
105
|
+
* Model and Controller test classes can both go in the same file.
|
106
|
+
* The popular automated test runner <tt>autotest</tt> ships with a handler for Mosquito. Install the ZenTest gem and run the <tt>autotest</tt> command in the same folder as the <tt>public</tt> and <tt>test</tt> directories.
|
107
|
+
* A Sqlite3 :memory: database is automatically used for tests that require a database.
|
91
108
|
|
92
109
|
You can run your tests by executing the test file with Ruby or by running the autotest command with no arguments (from the ZenTest gem).
|
93
110
|
|
94
111
|
ruby test/test_blog.rb
|
95
112
|
|
96
|
-
|
113
|
+
or
|
97
114
|
|
98
115
|
autotest
|
99
116
|
|
117
|
+
== RSpec
|
118
|
+
|
119
|
+
Do you prefer RSpec syntax? You can get halfway there by putting this include in your test file:
|
120
|
+
|
121
|
+
require 'spec/test_case_adapter'
|
100
122
|
|
101
|
-
|
123
|
+
Then you can use <tt>should</tt> and <tt>should_not</tt> on objects inside your tests.
|
102
124
|
|
103
|
-
|
125
|
+
== Authors
|
104
126
|
|
127
|
+
Geoffrey Grosenbach http://topfunky.com, with a supporting act
|
128
|
+
from the little fairy http://julik.nl and the evil multipart generator
|
129
|
+
conceived by http://maxidoors.ru.
|
data/Rakefile
CHANGED
@@ -1,13 +1,40 @@
|
|
1
|
+
$: << 'lib'
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'hoe'
|
3
|
-
require 'lib/mosquito'
|
5
|
+
require './lib/mosquito'
|
6
|
+
|
7
|
+
# Disable spurious warnings when running tests, ActiveMagic cannot stand -w
|
8
|
+
Hoe::RUBY_FLAGS.replace ENV['RUBY_FLAGS'] || "-I#{%w(lib test).join(File::PATH_SEPARATOR)}" +
|
9
|
+
(Hoe::RUBY_DEBUG ? " #{RUBY_DEBUG}" : '')
|
4
10
|
|
5
11
|
Hoe.new('Mosquito', Mosquito::VERSION) do |p|
|
6
12
|
p.name = "mosquito"
|
7
|
-
p.author = "Geoffrey Grosenbach"
|
13
|
+
p.author = ["Geoffrey Grosenbach"] # TODO Add Julik ...
|
8
14
|
p.description = "A library for writing tests for your Camping app."
|
9
15
|
p.email = 'boss@topfunky.com'
|
10
16
|
p.summary = "A Camping test library."
|
17
|
+
p.changes = p.paragraphs_of('CHANGELOG', 0..1).join("\n\n")
|
11
18
|
p.url = "http://mosquito.rubyforge.org"
|
19
|
+
p.rdoc_pattern = /README|CHANGELOG|mosquito/
|
20
|
+
p.clean_globs = ['**.log', 'coverage', 'coverage.data', 'test/test.log', 'email.txt']
|
12
21
|
p.extra_deps = ['activerecord', 'activesupport', 'camping']
|
13
22
|
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
require 'rcov/rcovtask'
|
26
|
+
desc "just rcov minus html output"
|
27
|
+
Rcov::RcovTask.new do |t|
|
28
|
+
t.test_files = FileList["test/test_*.rb"]
|
29
|
+
t.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Aggregate code coverage for unit, functional and integration tests'
|
33
|
+
Rcov::RcovTask.new("coverage") do |t|
|
34
|
+
t.test_files = FileList["test/test_*.rb"]
|
35
|
+
t.output_dir = "coverage"
|
36
|
+
t.verbose = true
|
37
|
+
t.rcov_opts << '--aggregate coverage.data'
|
38
|
+
end
|
39
|
+
rescue LoadError
|
40
|
+
end
|
data/lib/mosquito.rb
CHANGED
@@ -3,26 +3,60 @@ rubygems
|
|
3
3
|
test/unit
|
4
4
|
active_record
|
5
5
|
active_record/fixtures
|
6
|
-
active_support/binding_of_caller
|
7
6
|
camping
|
8
7
|
camping/session
|
9
8
|
fileutils
|
9
|
+
tempfile
|
10
10
|
stringio
|
11
|
-
cgi
|
12
11
|
).each { |lib| require lib }
|
13
12
|
|
14
13
|
module Mosquito
|
15
|
-
VERSION = '0.1.
|
14
|
+
VERSION = '0.1.3'
|
15
|
+
|
16
|
+
# For various methods that need to generate random text
|
17
|
+
def self.garbage(amount) #:nodoc:
|
18
|
+
fills = ("a".."z").to_a
|
19
|
+
str = (0...amount).map do
|
20
|
+
v = fills[rand(fills.length)]
|
21
|
+
(rand(2).zero? ? v.upcase : v)
|
22
|
+
end
|
23
|
+
str.join
|
24
|
+
end
|
25
|
+
|
26
|
+
# Will be raised if you try to test for something Camping does not support.
|
27
|
+
# Kind of a safeguard in the deep ocean of metaified Ruby goodness.
|
28
|
+
class SageAdvice < RuntimeError; end
|
29
|
+
|
30
|
+
# Will be raised if you try to call an absolute, canonical URL (with scheme and server).
|
31
|
+
# and the server does not match the specified request.
|
32
|
+
class NonLocalRequest < RuntimeError; end
|
33
|
+
|
34
|
+
def self.stash(something) #:nodoc:
|
35
|
+
@stashed = something
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.unstash #:nodoc:
|
39
|
+
x, @stashed = @stashed, nil; x
|
40
|
+
end
|
16
41
|
end
|
17
42
|
|
18
43
|
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
|
19
|
-
ActiveRecord::Base.logger = Logger.new("test/test.log")
|
20
|
-
Camping::Models::Session.create_schema
|
44
|
+
ActiveRecord::Base.logger = Logger.new("test/test.log") rescue Logger.new("test.log")
|
21
45
|
|
46
|
+
# This needs to be set relative to the file where the test comes from, NOT relative to the
|
47
|
+
# mosquito itself
|
22
48
|
Test::Unit::TestCase.fixture_path = "test/fixtures/"
|
23
49
|
|
24
50
|
class Test::Unit::TestCase #:nodoc:
|
25
51
|
def create_fixtures(*table_names)
|
52
|
+
if block_given?
|
53
|
+
self.class.fixtures(*table_names) { |*anything| yield(*anything) }
|
54
|
+
else
|
55
|
+
self.class.fixtures(*table_names)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.fixtures(*table_names)
|
26
60
|
if block_given?
|
27
61
|
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
|
28
62
|
else
|
@@ -30,76 +64,313 @@ class Test::Unit::TestCase #:nodoc:
|
|
30
64
|
end
|
31
65
|
end
|
32
66
|
|
67
|
+
##
|
68
|
+
# From Jay Fields.
|
69
|
+
#
|
70
|
+
# Allows tests to be specified as a block.
|
71
|
+
#
|
72
|
+
# test "should do this and that" do
|
73
|
+
# ...
|
74
|
+
# end
|
75
|
+
|
76
|
+
def self.test(name, &block)
|
77
|
+
test_name = :"test_#{name.gsub(' ','_')}"
|
78
|
+
raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
|
79
|
+
define_method test_name, &block
|
80
|
+
end
|
81
|
+
|
33
82
|
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
|
34
83
|
self.use_transactional_fixtures = true
|
35
84
|
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
|
36
85
|
self.use_instantiated_fixtures = false
|
37
86
|
end
|
38
87
|
|
39
|
-
|
88
|
+
# Mock request is used for composing the request body and headers
|
89
|
+
class Mosquito::MockRequest
|
90
|
+
# Should be a StringIO. However, you got some assignment methods that will
|
91
|
+
# stuff it with encoded parameters for you
|
92
|
+
attr_accessor :body
|
93
|
+
|
94
|
+
DEFAULT_HEADERS = {
|
95
|
+
'SERVER_NAME' => 'test.host',
|
96
|
+
'PATH_INFO' => '',
|
97
|
+
'HTTP_ACCEPT_ENCODING' => 'gzip,deflate',
|
98
|
+
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0',
|
99
|
+
'SCRIPT_NAME' => '/',
|
100
|
+
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
101
|
+
'HTTP_CACHE_CONTROL' => 'max-age=0',
|
102
|
+
'HTTP_ACCEPT_LANGUAGE' => 'en,ja;q=0.9,fr;q=0.9,de;q=0.8,es;q=0.7,it;q=0.7,nl;q=0.6,sv;q=0.5,nb;q=0.5,da;q=0.4,fi;q=0.3,pt;q=0.3,zh-Hans;q=0.2,zh-Hant;q=0.1,ko;q=0.1',
|
103
|
+
'HTTP_HOST' => 'test.host',
|
104
|
+
'REMOTE_ADDR' => '127.0.0.1',
|
105
|
+
'SERVER_SOFTWARE' => 'Mongrel 0.3.12.4',
|
106
|
+
'HTTP_KEEP_ALIVE' => '300',
|
107
|
+
'HTTP_REFERER' => 'http://localhost/',
|
108
|
+
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
109
|
+
'HTTP_VERSION' => 'HTTP/1.1',
|
110
|
+
'REQUEST_URI' => '/',
|
111
|
+
'SERVER_PORT' => '80',
|
112
|
+
'GATEWAY_INTERFACE' => 'CGI/1.2',
|
113
|
+
'HTTP_ACCEPT' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
|
114
|
+
'HTTP_CONNECTION' => 'keep-alive',
|
115
|
+
'REQUEST_METHOD' => 'GET',
|
116
|
+
}
|
117
|
+
|
40
118
|
def initialize
|
41
|
-
@headers =
|
42
|
-
|
43
|
-
'PATH_INFO' => '',
|
44
|
-
'HTTP_ACCEPT_ENCODING' => 'gzip,deflate',
|
45
|
-
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0',
|
46
|
-
'SCRIPT_NAME' => '/',
|
47
|
-
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
48
|
-
'HTTP_CACHE_CONTROL' => 'max-age=0',
|
49
|
-
'HTTP_ACCEPT_LANGUAGE' => 'en,ja;q=0.9,fr;q=0.9,de;q=0.8,es;q=0.7,it;q=0.7,nl;q=0.6,sv;q=0.5,nb;q=0.5,da;q=0.4,fi;q=0.3,pt;q=0.3,zh-Hans;q=0.2,zh-Hant;q=0.1,ko;q=0.1',
|
50
|
-
'HTTP_HOST' => 'localhost',
|
51
|
-
'REMOTE_ADDR' => '127.0.0.1',
|
52
|
-
'SERVER_SOFTWARE' => 'Mongrel 0.3.12.4',
|
53
|
-
'HTTP_KEEP_ALIVE' => '300',
|
54
|
-
'HTTP_REFERER' => 'http://localhost/',
|
55
|
-
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
56
|
-
'HTTP_VERSION' => 'HTTP/1.1',
|
57
|
-
'REQUEST_URI' => '/',
|
58
|
-
'SERVER_PORT' => '80',
|
59
|
-
'GATEWAY_INTERFACE' => 'CGI/1.2',
|
60
|
-
'HTTP_ACCEPT' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
|
61
|
-
'HTTP_CONNECTION' => 'keep-alive',
|
62
|
-
'REQUEST_METHOD' => 'GET',
|
63
|
-
}
|
64
|
-
end
|
65
|
-
|
66
|
-
def set(key, val)
|
67
|
-
@headers[key] = val
|
119
|
+
@headers = DEFAULT_HEADERS.with_indifferent_access # :-)
|
120
|
+
@body = StringIO.new('hello Camping')
|
68
121
|
end
|
69
|
-
|
122
|
+
|
123
|
+
# Returns the hash of headers
|
70
124
|
def to_hash
|
71
125
|
@headers
|
72
126
|
end
|
73
127
|
|
128
|
+
# Gets a header
|
74
129
|
def [](key)
|
75
130
|
@headers[key]
|
76
131
|
end
|
77
|
-
|
132
|
+
|
133
|
+
# Sets a header
|
78
134
|
def []=(key, value)
|
79
135
|
@headers[key] = value
|
80
136
|
end
|
137
|
+
alias_method :set, :[]=
|
138
|
+
|
139
|
+
# Retrieve a composed query string (including the eventual "?") with URL-escaped segments
|
140
|
+
def query_string
|
141
|
+
(@query_string_with_qmark.blank? ? '' : @query_string_with_qmark)
|
142
|
+
end
|
81
143
|
|
82
|
-
|
144
|
+
# Set a composed query string, should have URL-escaped segments and include the elements after the "?"
|
145
|
+
def query_string=(nqs)
|
146
|
+
@query_string_with_qmark = nqs.gsub(/^([^\?])/, '?\1')
|
147
|
+
@headers["REQUEST_URI"] = @headers["REQUEST_URI"].split(/\?/).shift + @query_string_with_qmark
|
148
|
+
if nqs.blank?
|
149
|
+
@headers.delete "QUERY_STRING"
|
150
|
+
else
|
151
|
+
@headers["QUERY_STRING"] = nqs.gsub(/^\?/, '')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Retrieve the domain (analogous to HTTP_HOST)
|
156
|
+
def domain
|
157
|
+
server_name || http_host
|
158
|
+
end
|
159
|
+
|
160
|
+
# Set the domain (changes both HTTP_HOST and SERVER_NAME)
|
161
|
+
def domain=(nd)
|
162
|
+
self['SERVER_NAME'] = self['HTTP_HOST'] = nd
|
163
|
+
end
|
164
|
+
|
83
165
|
# Allow getters like this:
|
84
|
-
# o.REQUEST_METHOD
|
85
|
-
|
166
|
+
# o.REQUEST_METHOD or o.request_method
|
86
167
|
def method_missing(method_name, *args)
|
87
|
-
|
88
|
-
|
168
|
+
triables = [method_name.to_s, method_name.to_s.upcase, "HTTP_" + method_name.to_s.upcase]
|
169
|
+
triables.map do | possible_key |
|
170
|
+
return @headers[possible_key] if @headers.has_key?(possible_key)
|
171
|
+
end
|
172
|
+
super(method_name, args)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Assign a hash of parameters that should be used for the query string
|
176
|
+
def query_string_params=(new_param_hash)
|
177
|
+
self.query_string = qs_build(new_param_hash)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Append a freeform segment to the query string in the request. Useful when you
|
181
|
+
# want to quickly combine the query strings.
|
182
|
+
def append_to_query_string(piece)
|
183
|
+
new_qs = '?' + [self.query_string.gsub(/^\?/, ''), piece].reject{|e| e.blank? }.join('&')
|
184
|
+
self.query_string = new_qs
|
185
|
+
end
|
186
|
+
|
187
|
+
# Assign a hash of parameters that should be used for POST. These might include
|
188
|
+
# objects that act like a file upload (with #original_filename and all)
|
189
|
+
def post_params=(new_param_hash_or_str)
|
190
|
+
# First see if this is a body payload
|
191
|
+
if !new_param_hash_or_str.kind_of?(Hash)
|
192
|
+
compose_verbatim_payload(new_param_hash_or_str)
|
193
|
+
# then check if anything in the new param hash resembles an uplaod
|
194
|
+
elsif extract_values(new_param_hash_or_str).any?{|value| value.respond_to?(:original_filename) }
|
195
|
+
compose_multipart_params(new_param_hash_or_str)
|
89
196
|
else
|
90
|
-
|
197
|
+
compose_urlencoded_params(new_param_hash_or_str)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Generates a random 22-character MIME boundary (useful for composing multipart POSTs)
|
202
|
+
def generate_boundary
|
203
|
+
"msqto-" + Mosquito::garbage(16)
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
# Quickly URL-escape something
|
208
|
+
def esc(t); Camping.escape(t.to_s);end
|
209
|
+
|
210
|
+
# Extracts an array of values from a deeply-nested hash
|
211
|
+
def extract_values(hash_or_a)
|
212
|
+
returning([]) do | vals |
|
213
|
+
flatten_hash(hash_or_a) do | keys, value |
|
214
|
+
vals << value
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Configures the test request for a POST
|
220
|
+
def compose_urlencoded_params(new_param_hash)
|
221
|
+
self['REQUEST_METHOD'] = 'POST'
|
222
|
+
self['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
223
|
+
@body = StringIO.new(qs_build(new_param_hash))
|
224
|
+
end
|
225
|
+
|
226
|
+
def compose_verbatim_payload(payload)
|
227
|
+
self['REQUEST_METHOD'] = 'POST'
|
228
|
+
self['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
229
|
+
@body = StringIO.new(payload)
|
91
230
|
end
|
231
|
+
|
232
|
+
# Configures the test request for a multipart POST
|
233
|
+
def compose_multipart_params(new_param_hash)
|
234
|
+
# here we check if the encoded segments contain the boundary and should generate a new one
|
235
|
+
# if something is matched
|
236
|
+
boundary = "----------#{generate_boundary}"
|
237
|
+
self['REQUEST_METHOD'] = 'POST'
|
238
|
+
self['CONTENT_TYPE'] = "multipart/form-data; boundary=#{boundary}"
|
239
|
+
@body = StringIO.new(multipart_build(new_param_hash, boundary))
|
240
|
+
end
|
241
|
+
|
242
|
+
# Return a multipart value segment from a file upload handle.
|
243
|
+
def uploaded_file_segment(key, upload_io, boundary)
|
244
|
+
<<-EOF
|
245
|
+
--#{boundary}\r
|
246
|
+
Content-Disposition: form-data; name="#{key}"; filename="#{Camping.escape(upload_io.original_filename)}"\r
|
247
|
+
Content-Type: #{upload_io.content_type}\r
|
248
|
+
Content-Length: #{upload_io.size}\r
|
249
|
+
\r
|
250
|
+
#{upload_io.read}\r
|
251
|
+
EOF
|
252
|
+
end
|
253
|
+
|
254
|
+
# Return a conventional value segment from a parameter value
|
255
|
+
def conventional_segment(key, value, boundary)
|
256
|
+
<<-EOF
|
257
|
+
--#{boundary}\r
|
258
|
+
Content-Disposition: form-data; name="#{key}"\r
|
259
|
+
\r
|
260
|
+
#{value}\r
|
261
|
+
EOF
|
262
|
+
end
|
263
|
+
|
264
|
+
# Build a multipart request body that includes both uploaded files and conventional parameters.
|
265
|
+
# To have predictable results we sort the output segments (a hash passed in will not be
|
266
|
+
# iterated over in the original definition order anyway, as a good developer should know)
|
267
|
+
def multipart_build(params, boundary)
|
268
|
+
flat = []
|
269
|
+
flatten_hash(params) do | keys, value |
|
270
|
+
if keys[-1].nil? # warn the user that Camping will never see that
|
271
|
+
raise Mosquito::SageAdvice,
|
272
|
+
"Camping will only show you the last element of the array when using multipart forms"
|
273
|
+
end
|
274
|
+
|
275
|
+
flat_key = [esc(keys.shift), keys.map{|k| "[%s]" % esc(k) }].flatten.join
|
276
|
+
if value.respond_to?(:original_filename)
|
277
|
+
flat << uploaded_file_segment(flat_key, value, boundary)
|
278
|
+
else
|
279
|
+
flat << conventional_segment(flat_key, value, boundary)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
flat.sort.join("")+"--#{boundary}--\r"
|
283
|
+
end
|
284
|
+
|
285
|
+
# Build a query string. The brackets are NOT encoded. Camping is peculiar in that
|
286
|
+
# in contrast to Rails it wants item=1&item=2 to make { item=>[1,2] } to make arrays. We have
|
287
|
+
# to account for that.
|
288
|
+
def qs_build (hash)
|
289
|
+
returning([]) do | qs |
|
290
|
+
flatten_hash(hash) do | keys, value |
|
291
|
+
keys.pop if keys[-1].nil? # cater for camping array handling
|
292
|
+
if value.respond_to?(:original_filename)
|
293
|
+
raise Mosquito::SageAdvice, "Sending a file using GET won't do you any good"
|
294
|
+
end
|
295
|
+
|
296
|
+
qs << [esc(keys.shift), keys.map{|k| "[%s]" % esc(k)}, '=', esc(value)].flatten.join
|
297
|
+
end
|
298
|
+
end.sort.join('&')
|
299
|
+
end
|
300
|
+
|
301
|
+
# Will accept a hash or array of any depth, collapse it into
|
302
|
+
# pairs in the form of ([first_level_k, second_level_k, ...], value)
|
303
|
+
# and yield these pairs as it goes to the supplied block. Some
|
304
|
+
# pairs might be yieled twice because arrays create repeating keys.
|
305
|
+
def flatten_hash(hash_or_a, parent_keys = [], &blk)
|
306
|
+
if hash_or_a.is_a?(Hash)
|
307
|
+
hash_or_a.each_pair do | k, v |
|
308
|
+
flatten_hash(v, parent_keys + [k], &blk)
|
309
|
+
end
|
310
|
+
elsif hash_or_a.is_a?(Array)
|
311
|
+
hash_or_a.map do | v |
|
312
|
+
blk.call(parent_keys + [nil], v)
|
313
|
+
end
|
314
|
+
else
|
315
|
+
blk.call(parent_keys, hash_or_a)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Works like a wrapper for a simulated file upload. To use:
|
321
|
+
#
|
322
|
+
# uploaded = Mosquito::MockUpload.new("beach.jpg")
|
323
|
+
#
|
324
|
+
# This will create a file with the JPEG content-type and 122 bytes of purely random data, which
|
325
|
+
# can then be submitted as a part of the test request
|
326
|
+
class Mosquito::MockUpload < StringIO
|
327
|
+
attr_reader :local_path, :original_filename, :content_type, :extension
|
328
|
+
IMAGE_TYPES = {:jpg => 'image/jpeg', :png => 'image/png', :gif => 'image/gif',
|
329
|
+
:pdf => 'application/pdf' }.stringify_keys
|
330
|
+
|
331
|
+
def initialize(filename)
|
332
|
+
tempname = "tempfile_#{Time.now.to_i}"
|
333
|
+
|
334
|
+
@temp = ::Tempfile.new(tempname)
|
335
|
+
@local_path = @temp.path
|
336
|
+
@original_filename = File.basename(filename)
|
337
|
+
@extension = File.extname(@original_filename).gsub(/^\./, '').downcase
|
338
|
+
@content_type = IMAGE_TYPES[@extension] || "application/#{@extension}"
|
339
|
+
|
340
|
+
size = 100.bytes
|
341
|
+
super("Stub file %s \n%s\n" % [@original_filename, Mosquito::garbage(size)])
|
342
|
+
end
|
343
|
+
|
344
|
+
def inspect
|
345
|
+
info = " @size='#{length}' @filename='#{original_filename}' @content_type='#{content_type}'>"
|
346
|
+
super[0..-2] + info
|
92
347
|
end
|
93
348
|
|
94
349
|
end
|
95
350
|
|
351
|
+
# Stealing our assigns the evil way. This should pose no problem
|
352
|
+
# for things that happen in the controller actions, but might be tricky
|
353
|
+
# if some other service upstream munges the variables.
|
354
|
+
# This service will always get included last (innermost), so it runs regardless of
|
355
|
+
# the services upstream (such as HTTP auth) that might not call super
|
356
|
+
module Mosquito::Proboscis #:nodoc:
|
357
|
+
def service(*a)
|
358
|
+
returning(super(*a)) do
|
359
|
+
a = instance_variables.inject({}) do | assigns, ivar |
|
360
|
+
assigns[ivar.gsub(/^@/, '')] = instance_variable_get(ivar); assigns
|
361
|
+
end
|
362
|
+
Mosquito.stash(::Camping::H[a])
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
96
366
|
|
97
367
|
module Camping
|
98
368
|
|
99
369
|
class Test < Test::Unit::TestCase
|
100
370
|
|
101
|
-
def test_dummy; end
|
371
|
+
def test_dummy; end #:nodoc
|
102
372
|
|
373
|
+
# The reverse of the reverse of the reverse of assert(condition)
|
103
374
|
def deny(condition, message='')
|
104
375
|
assert !condition, message
|
105
376
|
end
|
@@ -122,64 +393,110 @@ module Camping
|
|
122
393
|
yield
|
123
394
|
assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
|
124
395
|
end
|
396
|
+
|
397
|
+
# See +assert_difference+
|
125
398
|
def assert_no_difference(object, method, &block)
|
126
399
|
assert_difference object, method, 0, &block
|
127
400
|
end
|
128
|
-
|
129
401
|
end
|
130
|
-
|
131
|
-
|
132
|
-
|
402
|
+
|
403
|
+
# Used to test the controllers and rendering. The test should be called <App>Test
|
404
|
+
# (BlogTest for the aplication called Blog). A number of helper instance variables
|
405
|
+
# will be created for you - @request, which will contain a Mosquito::MockRequest
|
406
|
+
# object, @response (contains the response with headers and body), @cookies (a hash)
|
407
|
+
# and @state (a hash). Request and response will be reset in each test.
|
408
|
+
class WebTest < Test
|
409
|
+
|
410
|
+
# Gives you access to the instance variables assigned by the controller
|
411
|
+
attr_reader :assigns
|
412
|
+
|
413
|
+
def test_dummy; end #:nodoc
|
414
|
+
|
133
415
|
def setup
|
134
416
|
@class_name_abbr = self.class.name.gsub(/^Test/, '')
|
135
|
-
@request = MockRequest.new
|
136
|
-
@cookies
|
417
|
+
@request = Mosquito::MockRequest.new
|
418
|
+
@cookies, @response, @assigns = {}, {}, {}
|
137
419
|
end
|
138
|
-
|
420
|
+
|
421
|
+
# Send a GET request to a URL
|
139
422
|
def get(url='/', vars={})
|
140
423
|
send_request url, vars, 'GET'
|
141
424
|
end
|
142
425
|
|
426
|
+
# Send a POST request to a URL. All requests except GET will allow
|
427
|
+
# setting verbatim URL-encoded parameters as the third argument instead
|
428
|
+
# of a hash.
|
143
429
|
def post(url, post_vars={})
|
144
430
|
send_request url, post_vars, 'POST'
|
145
431
|
end
|
146
432
|
|
433
|
+
# Send a DELETE request to a URL. All requests except GET will allow
|
434
|
+
# setting verbatim URL-encoded parameters as the third argument instead
|
435
|
+
# of a hash.
|
147
436
|
def delete(url, vars={})
|
148
437
|
send_request url, vars, 'DELETE'
|
149
438
|
end
|
150
439
|
|
440
|
+
# Send a PUT request to a URL. All requests except GET will allow
|
441
|
+
# setting verbatim URL-encoded parameters as the third argument instead
|
442
|
+
# of a hash.
|
151
443
|
def put(url, vars={})
|
152
444
|
send_request url, vars, 'PUT'
|
153
445
|
end
|
154
446
|
|
447
|
+
# Send any request. We will try to guess what you meant - if there are uploads to be
|
448
|
+
# processed it's not going to be a GET, that's for sure.
|
155
449
|
def send_request(url, post_vars, method)
|
450
|
+
|
451
|
+
if method.to_s.downcase == "get"
|
452
|
+
@request.query_string_params = post_vars
|
453
|
+
else
|
454
|
+
@request.post_params = post_vars
|
455
|
+
end
|
456
|
+
|
457
|
+
# If there is some stuff in the URL to be used as a query string, why ignore it?
|
458
|
+
url, qs_from_url = url.split(/\?/)
|
459
|
+
|
460
|
+
relativize_url!(url)
|
461
|
+
|
462
|
+
@request.append_to_query_string(qs_from_url) if qs_from_url
|
463
|
+
|
464
|
+
# We do allow the user to override that one
|
156
465
|
@request['REQUEST_METHOD'] = method
|
466
|
+
|
157
467
|
@request['SCRIPT_NAME'] = '/' + @class_name_abbr.downcase
|
158
468
|
@request['PATH_INFO'] = '/' + url
|
159
|
-
|
160
|
-
|
161
|
-
@request['
|
162
|
-
|
163
|
-
@response = eval("#{@class_name_abbr}.run StringIO.new('#{qs_build(post_vars)}'), @request")
|
164
|
-
|
165
|
-
@cookies = @response.headers['Set-Cookie'].inject(@cookies||{}) do |res,header|
|
166
|
-
data = header.split(';').first
|
167
|
-
name, value = data.split('=')
|
168
|
-
res[name] = value
|
169
|
-
res
|
469
|
+
|
470
|
+
@request['REQUEST_URI'] = [@request.SCRIPT_NAME, @request.PATH_INFO].join('').squeeze('/')
|
471
|
+
unless @request['QUERY_STRING'].blank?
|
472
|
+
@request['REQUEST_URI'] += ('?' + @request['QUERY_STRING'])
|
170
473
|
end
|
171
474
|
|
172
|
-
|
475
|
+
if @cookies
|
476
|
+
@request['HTTP_COOKIE'] = @cookies.map {|k,v| "#{k}=#{Camping.escape(v)}" }.join('; ')
|
477
|
+
end
|
478
|
+
|
479
|
+
# Inject the proboscis if we haven't already done so
|
480
|
+
pr = Mosquito::Proboscis
|
481
|
+
eval("#{@class_name_abbr}.send(:include, pr) unless #{@class_name_abbr}.ancestors.include?(pr)")
|
482
|
+
|
483
|
+
# Run the request
|
484
|
+
@response = eval("#{@class_name_abbr}.run @request.body, @request")
|
485
|
+
@assigns = Mosquito::unstash
|
486
|
+
|
487
|
+
# We need to restore the cookies separately so that the app
|
488
|
+
# restores our session on the next request. We retrieve cookies and
|
489
|
+
# the session in their assigned form instead of parsing the headers and
|
490
|
+
# doing a deserialization cycle
|
491
|
+
@cookies = @assigns[:cookies] || H[{}]
|
492
|
+
@state = @assigns[:state] || H[{}]
|
173
493
|
|
174
|
-
session = Camping::Models::Session.persist @cookies
|
175
|
-
app = @class_name_abbr.gsub(/^(\w+)::.+$/, '\1')
|
176
|
-
@state = (session[app] ||= Camping::H[])
|
177
|
-
|
178
494
|
if @response.headers['X-Sendfile']
|
179
495
|
@response.body = File.read(@response.headers['X-Sendfile'])
|
180
496
|
end
|
181
497
|
end
|
182
|
-
|
498
|
+
|
499
|
+
# Assert a specific response (:success, :error or a freeform error code as integer)
|
183
500
|
def assert_response(status_code)
|
184
501
|
case status_code
|
185
502
|
when :success
|
@@ -187,43 +504,83 @@ module Camping
|
|
187
504
|
when :redirect
|
188
505
|
assert_equal 302, @response.status
|
189
506
|
when :error
|
190
|
-
assert @response.status >= 500
|
507
|
+
assert @response.status >= 500,
|
508
|
+
"Response status should have been >= 500 but was #{@response.status}"
|
191
509
|
else
|
192
510
|
assert_equal status_code, @response.status
|
193
511
|
end
|
194
512
|
end
|
195
|
-
|
513
|
+
|
514
|
+
# Check that the text in the body matches a regexp
|
196
515
|
def assert_match_body(regex, message=nil)
|
197
516
|
assert_match regex, @response.body, message
|
198
517
|
end
|
518
|
+
|
519
|
+
# Opposite of +assert_match_body+
|
199
520
|
def assert_no_match_body(regex, message=nil)
|
200
521
|
assert_no_match regex, @response.body, message
|
201
522
|
end
|
202
|
-
|
523
|
+
|
524
|
+
# Make sure that we are redirected to a certain URL. It's not needed
|
525
|
+
# to prepend the URL with a mount (instead of "/blog/latest-news" you can use "/latest-news")
|
526
|
+
#
|
527
|
+
# Checks both the response status and the url.
|
203
528
|
def assert_redirected_to(url, message=nil)
|
204
|
-
|
529
|
+
assert_response :redirect
|
530
|
+
assert_equal url, extract_redirection_url, message
|
205
531
|
end
|
206
|
-
|
532
|
+
|
533
|
+
# Assert that a cookie of name matches a certain pattern
|
207
534
|
def assert_cookie(name, pat, message=nil)
|
208
535
|
assert_match pat, @cookies[name], message
|
209
536
|
end
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
def qs_build(var_hash)
|
216
|
-
var_hash.map do |k, v|
|
217
|
-
[Camping.escape(k.to_s), Camping.escape(v.to_s)].join('=')
|
218
|
-
end.join('&')
|
537
|
+
|
538
|
+
# Nothing is new under the sun
|
539
|
+
def follow_redirect
|
540
|
+
get extract_redirection_url
|
219
541
|
end
|
220
|
-
|
542
|
+
|
543
|
+
# Quickly gives you a handle to a file with random content
|
544
|
+
def upload(filename)
|
545
|
+
Mosquito::MockUpload.new(filename)
|
546
|
+
end
|
547
|
+
|
548
|
+
# Checks that Camping sent us a cookie to attach a session
|
549
|
+
def assert_session_started
|
550
|
+
assert_not_nil @cookies["camping_sid"],
|
551
|
+
"The session ID cookie was empty although session should have started"
|
552
|
+
end
|
553
|
+
|
554
|
+
# The reverse of +assert_session_started+
|
555
|
+
def assert_no_session
|
556
|
+
assert_nil @cookies["camping_sid"],
|
557
|
+
"A session cookie was sent although this should not happen"
|
558
|
+
end
|
559
|
+
|
560
|
+
private
|
561
|
+
def extract_redirection_url
|
562
|
+
loc = @response.headers['Location']
|
563
|
+
path_seg = @response.headers['Location'].path.gsub(%r!/#{@class_name_abbr.downcase}!, '')
|
564
|
+
loc.query ? (path_seg + "?" + loc.query).to_s : path_seg.to_s
|
565
|
+
end
|
566
|
+
|
567
|
+
def relativize_url!(url)
|
568
|
+
return unless url =~ /^([a-z]+):\//
|
569
|
+
p = URI.parse(url)
|
570
|
+
unless p.host == @request.domain
|
571
|
+
raise ::Mosquito::NonLocalRequest,
|
572
|
+
"You tried to callout to #{p} which is outside of the test domain"
|
573
|
+
end
|
574
|
+
url.replace(p.path + (p.query.blank ? '' : "?#{p.query}"))
|
575
|
+
end
|
221
576
|
end
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
def test_dummy; end
|
226
|
-
|
577
|
+
|
578
|
+
# Used to test the models - no infrastructure will be created for running the request
|
579
|
+
class ModelTest < Test
|
580
|
+
def test_dummy; end #:nodoc
|
227
581
|
end
|
228
|
-
|
582
|
+
|
583
|
+
# Deprecated but humane
|
584
|
+
UnitTest = ModelTest
|
585
|
+
FunctionalTest = WebTest
|
229
586
|
end
|