Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Reorganized to standard svn structure

git-svn-id: http://topfunky.net/svn/mosquito/trunk@57 a47d2692-3f15-0410-91a4-aff73ff2e1db
  • Loading branch information...
commit fa5022ba3e5f071a846f96fb477c6b93abb14b50 0 parents
topfunky authored
30 CHANGELOG
@@ -0,0 +1,30 @@
+== 0.1.3 - The little fairy release
+
+* You can use an absolute URL with scheme and all instead of path-only abbreviation
+* You can now assign a URL-encoded payload instead of a hash when doing all requests
+ except of a GET. This to be nice to people building web-service backends.
+* Be nice to the folks that do not use the database or sessions
+* we have @assigns to access the instance variables summoned in the controller
+* follow_redirect makes use of the feature below, accordingly
+* You can now pass verbatim query string parameters like so
+ get "/blog/archive?page=2"
+ which will be conveniently mixed with other params (and can also be used when doing POSTs!)
+* Rdoc is extremely unfriendly to pluses and stars in Unicode mode. They should be punished.
+* FunctionalTest is now WebTest and UnitTest is now ModelTest, because the ruby sadists said they shall be.
+* We now support proper, infinitely nested and encapsulated parameters
+ * for querystrings
+ * for postvars
+ * and yes, for uploads too
+* 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.
+* We are Camping 1.5 compatible
+* You can now do 'test "should do this"' and pass a block of assertions.
+* More tests for better coverage of mosquito.rb
+* Cleanup of Rakefile with other options and proper CHANGELOG inclusion:CHANGELOG
+
+== 0.1.2
+
+== 0.1.1
+
+* Added dependencies to Rakefile (active_record, active_support, camping)
+* Added PUT and DELETE methods [cdcarter]
+* Params can be passed when testing GET [cdcarter]
21 MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2005 Geoffrey Grosenbach boss@topfunky.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
20 Manifest.txt
@@ -0,0 +1,20 @@
+CHANGELOG
+MIT-LICENSE
+Manifest.txt
+README.txt
+Rakefile
+lib/mosquito.rb
+public/bare.rb
+public/blog.rb
+public/blog/controllers.rb
+public/blog/models.rb
+public/blog/views.rb
+test/fixtures/blog_comments.yml
+test/fixtures/blog_posts.yml
+test/fixtures/blog_users.yml
+test/sage_advice_cases/parsing_arrays.rb
+test/test_bare.rb
+test/test_blog.rb
+test/test_helpers.rb
+test/test_mock_request.rb
+test/test_mock_upload.rb
129 README.txt
@@ -0,0 +1,129 @@
+=Mosquito, for Bug-Free Camping
+
+A testing helper for those times when you go Camping.
+Apply on the face, neck, and any exposed areas such as your
+Models and Controllers. Scrub gently, observe the results.
+
+== Usage
+
+Make a few files and directories like this:
+
+ public/
+ blog.rb
+ test/
+ test_blog.rb
+ fixtures/
+ blog_comments.yml
+ blog_posts.yml
+ blog_users.yml
+
+Setup <b>test_blog.rb</b> like this:
+
+ require 'rubygems'
+ require 'mosquito'
+ require File.dirname(__FILE__) + "/../public/blog"
+
+ Blog.create
+ include Blog::Models
+
+ class TestBlog < Camping::WebTest
+
+ fixtures :blog_posts, :blog_users, :blog_comments
+
+ def setup
+ super
+ # Do other stuff here
+ end
+
+ test "should get index" do
+ get
+ assert_response :success
+ assert_match_body %r!>blog<!
+ end
+
+ test "should get view" do
+ get '/view/1'
+ assert_response :success
+ assert_kind_of Article, @assigns[:article]
+ assert_match_body %r!The quick fox jumped over the lazy dog!
+ end
+
+ test "should change profile" do
+ @request['SERVER_NAME'] = 'jonh.blogs.net'
+ post '/change-profile', :new_photo => upload("picture.jpg")
+ assert_response :success
+ assert_match_body %r!The pic has been uploaded!
+ end
+ end
+
+ # A unit test
+ class TestPost < Camping::ModelTest
+
+ fixtures :blog_posts, :blog_users, :blog_comments
+
+ test "should create" do
+ post = Post.create( :user_id => 1,
+ :title => "Title",
+ :body => "Body")
+ assert post.valid?
+ end
+
+ test "should be associated with User" do
+ post = Post.find :first
+ assert_kind_of User, post.user
+ assert_equal 1, post.user.id
+ end
+
+ end
+
+You can also use old-school methods like <tt>def test_create</tt>, but we think this way is much more natural.
+
+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.
+
+== Details
+
+Inherit from Camping::WebTest or Camping::ModelTest. If you define <tt>setup</tt>,
+be sure to call <tt>super</tt> so the parent class can do its thing.
+
+You should also call the <tt>MyApp.create</tt> method if you have one, <b>yourself</b>. You will also
+need to <tt>include MyApp::Models</tt> at the top of your test file if you want to use
+Models in your assertions directly (without going through MyApp::Models::SomeModel).
+
+Make fixtures in <b>test/fixtures</b>. Remember that Camping models use the name of
+the mount plus the model name: <b>blog_posts</b> for the <b>Post</b> model.
+
+See <b>blog_test.rb</b> for an example of both Web and Model tests.
+
+Mosquito is one file, just like your app (right?), so feel free to ship it included with the app itself
+to simplify testing.
+
+== Warning: You are Camping, not Rail-riding
+
+These directives are highly recommended when using Mosquito:
+
+* Test files start with <b>test_</b> (test_blog.rb). Test classes start with <b>Test</b> (TestBlog).
+* Model and Controller test classes can both go in the same file.
+* 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.
+* A Sqlite3 :memory: database is automatically used for tests that require a database.
+
+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).
+
+ ruby test/test_blog.rb
+
+or
+
+ autotest
+
+== RSpec
+
+Do you prefer RSpec syntax? You can get halfway there by putting this include in your test file:
+
+ require 'spec/test_case_adapter'
+
+Then you can use <tt>should</tt> and <tt>should_not</tt> on objects inside your tests.
+
+== Authors
+
+Geoffrey Grosenbach http://topfunky.com, with a supporting act
+from the little fairy http://julik.nl and the evil multipart generator
+conceived by http://maxidoors.ru.
40 Rakefile
@@ -0,0 +1,40 @@
+$: << 'lib'
+
+require 'rubygems'
+require 'hoe'
+require './lib/mosquito'
+
+# Disable spurious warnings when running tests, ActiveMagic cannot stand -w
+Hoe::RUBY_FLAGS.replace ENV['RUBY_FLAGS'] || "-I#{%w(lib test).join(File::PATH_SEPARATOR)}" +
+ (Hoe::RUBY_DEBUG ? " #{RUBY_DEBUG}" : '')
+
+Hoe.new('Mosquito', Mosquito::VERSION) do |p|
+ p.name = "mosquito"
+ p.author = ["Geoffrey Grosenbach"] # TODO Add Julik ...
+ p.description = "A library for writing tests for your Camping app."
+ p.email = 'boss@topfunky.com'
+ p.summary = "A Camping test library."
+ p.changes = p.paragraphs_of('CHANGELOG', 0..1).join("\n\n")
+ p.url = "http://mosquito.rubyforge.org"
+ p.rdoc_pattern = /README|CHANGELOG|mosquito/
+ p.clean_globs = ['**.log', 'coverage', 'coverage.data', 'test/test.log', 'email.txt']
+ p.extra_deps = ['activerecord', 'activesupport', 'camping']
+end
+
+begin
+ require 'rcov/rcovtask'
+ desc "just rcov minus html output"
+ Rcov::RcovTask.new do |t|
+ t.test_files = FileList["test/test_*.rb"]
+ t.verbose = true
+ end
+
+ desc 'Aggregate code coverage for unit, functional and integration tests'
+ Rcov::RcovTask.new("coverage") do |t|
+ t.test_files = FileList["test/test_*.rb"]
+ t.output_dir = "coverage"
+ t.verbose = true
+ t.rcov_opts << '--aggregate coverage.data'
+ end
+rescue LoadError
+end
586 lib/mosquito.rb
@@ -0,0 +1,586 @@
+%w(
+rubygems
+test/unit
+active_record
+active_record/fixtures
+camping
+camping/session
+fileutils
+tempfile
+stringio
+).each { |lib| require lib }
+
+module Mosquito
+ VERSION = '0.1.3'
+
+ # For various methods that need to generate random text
+ def self.garbage(amount) #:nodoc:
+ fills = ("a".."z").to_a
+ str = (0...amount).map do
+ v = fills[rand(fills.length)]
+ (rand(2).zero? ? v.upcase : v)
+ end
+ str.join
+ end
+
+ # Will be raised if you try to test for something Camping does not support.
+ # Kind of a safeguard in the deep ocean of metaified Ruby goodness.
+ class SageAdvice < RuntimeError; end
+
+ # Will be raised if you try to call an absolute, canonical URL (with scheme and server).
+ # and the server does not match the specified request.
+ class NonLocalRequest < RuntimeError; end
+
+ def self.stash(something) #:nodoc:
+ @stashed = something
+ end
+
+ def self.unstash #:nodoc:
+ x, @stashed = @stashed, nil; x
+ end
+end
+
+ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
+ActiveRecord::Base.logger = Logger.new("test/test.log") rescue Logger.new("test.log")
+
+# This needs to be set relative to the file where the test comes from, NOT relative to the
+# mosquito itself
+Test::Unit::TestCase.fixture_path = "test/fixtures/"
+
+class Test::Unit::TestCase #:nodoc:
+ def create_fixtures(*table_names)
+ if block_given?
+ self.class.fixtures(*table_names) { |*anything| yield(*anything) }
+ else
+ self.class.fixtures(*table_names)
+ end
+ end
+
+ def self.fixtures(*table_names)
+ if block_given?
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
+ else
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
+ end
+ end
+
+ ##
+ # From Jay Fields.
+ #
+ # Allows tests to be specified as a block.
+ #
+ # test "should do this and that" do
+ # ...
+ # end
+
+ def self.test(name, &block)
+ test_name = :"test_#{name.gsub(' ','_')}"
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
+ define_method test_name, &block
+ end
+
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
+ self.use_transactional_fixtures = true
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
+ self.use_instantiated_fixtures = false
+end
+
+# Mock request is used for composing the request body and headers
+class Mosquito::MockRequest
+ # Should be a StringIO. However, you got some assignment methods that will
+ # stuff it with encoded parameters for you
+ attr_accessor :body
+
+ DEFAULT_HEADERS = {
+ 'SERVER_NAME' => 'test.host',
+ 'PATH_INFO' => '',
+ 'HTTP_ACCEPT_ENCODING' => 'gzip,deflate',
+ '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',
+ 'SCRIPT_NAME' => '/',
+ 'SERVER_PROTOCOL' => 'HTTP/1.1',
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ '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',
+ 'HTTP_HOST' => 'test.host',
+ 'REMOTE_ADDR' => '127.0.0.1',
+ 'SERVER_SOFTWARE' => 'Mongrel 0.3.12.4',
+ 'HTTP_KEEP_ALIVE' => '300',
+ 'HTTP_REFERER' => 'http://localhost/',
+ 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'HTTP_VERSION' => 'HTTP/1.1',
+ 'REQUEST_URI' => '/',
+ 'SERVER_PORT' => '80',
+ 'GATEWAY_INTERFACE' => 'CGI/1.2',
+ 'HTTP_ACCEPT' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
+ 'HTTP_CONNECTION' => 'keep-alive',
+ 'REQUEST_METHOD' => 'GET',
+ }
+
+ def initialize
+ @headers = DEFAULT_HEADERS.with_indifferent_access # :-)
+ @body = StringIO.new('hello Camping')
+ end
+
+ # Returns the hash of headers
+ def to_hash
+ @headers
+ end
+
+ # Gets a header
+ def [](key)
+ @headers[key]
+ end
+
+ # Sets a header
+ def []=(key, value)
+ @headers[key] = value
+ end
+ alias_method :set, :[]=
+
+ # Retrieve a composed query string (including the eventual "?") with URL-escaped segments
+ def query_string
+ (@query_string_with_qmark.blank? ? '' : @query_string_with_qmark)
+ end
+
+ # Set a composed query string, should have URL-escaped segments and include the elements after the "?"
+ def query_string=(nqs)
+ @query_string_with_qmark = nqs.gsub(/^([^\?])/, '?\1')
+ @headers["REQUEST_URI"] = @headers["REQUEST_URI"].split(/\?/).shift + @query_string_with_qmark
+ if nqs.blank?
+ @headers.delete "QUERY_STRING"
+ else
+ @headers["QUERY_STRING"] = nqs.gsub(/^\?/, '')
+ end
+ end
+
+ # Retrieve the domain (analogous to HTTP_HOST)
+ def domain
+ server_name || http_host
+ end
+
+ # Set the domain (changes both HTTP_HOST and SERVER_NAME)
+ def domain=(nd)
+ self['SERVER_NAME'] = self['HTTP_HOST'] = nd
+ end
+
+ # Allow getters like this:
+ # o.REQUEST_METHOD or o.request_method
+ def method_missing(method_name, *args)
+ triables = [method_name.to_s, method_name.to_s.upcase, "HTTP_" + method_name.to_s.upcase]
+ triables.map do | possible_key |
+ return @headers[possible_key] if @headers.has_key?(possible_key)
+ end
+ super(method_name, args)
+ end
+
+ # Assign a hash of parameters that should be used for the query string
+ def query_string_params=(new_param_hash)
+ self.query_string = qs_build(new_param_hash)
+ end
+
+ # Append a freeform segment to the query string in the request. Useful when you
+ # want to quickly combine the query strings.
+ def append_to_query_string(piece)
+ new_qs = '?' + [self.query_string.gsub(/^\?/, ''), piece].reject{|e| e.blank? }.join('&')
+ self.query_string = new_qs
+ end
+
+ # Assign a hash of parameters that should be used for POST. These might include
+ # objects that act like a file upload (with #original_filename and all)
+ def post_params=(new_param_hash_or_str)
+ # First see if this is a body payload
+ if !new_param_hash_or_str.kind_of?(Hash)
+ compose_verbatim_payload(new_param_hash_or_str)
+ # then check if anything in the new param hash resembles an uplaod
+ elsif extract_values(new_param_hash_or_str).any?{|value| value.respond_to?(:original_filename) }
+ compose_multipart_params(new_param_hash_or_str)
+ else
+ compose_urlencoded_params(new_param_hash_or_str)
+ end
+ end
+
+ # Generates a random 22-character MIME boundary (useful for composing multipart POSTs)
+ def generate_boundary
+ "msqto-" + Mosquito::garbage(16)
+ end
+
+ private
+ # Quickly URL-escape something
+ def esc(t); Camping.escape(t.to_s);end
+
+ # Extracts an array of values from a deeply-nested hash
+ def extract_values(hash_or_a)
+ returning([]) do | vals |
+ flatten_hash(hash_or_a) do | keys, value |
+ vals << value
+ end
+ end
+ end
+
+ # Configures the test request for a POST
+ def compose_urlencoded_params(new_param_hash)
+ self['REQUEST_METHOD'] = 'POST'
+ self['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+ @body = StringIO.new(qs_build(new_param_hash))
+ end
+
+ def compose_verbatim_payload(payload)
+ self['REQUEST_METHOD'] = 'POST'
+ self['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+ @body = StringIO.new(payload)
+ end
+
+ # Configures the test request for a multipart POST
+ def compose_multipart_params(new_param_hash)
+ # here we check if the encoded segments contain the boundary and should generate a new one
+ # if something is matched
+ boundary = "----------#{generate_boundary}"
+ self['REQUEST_METHOD'] = 'POST'
+ self['CONTENT_TYPE'] = "multipart/form-data; boundary=#{boundary}"
+ @body = StringIO.new(multipart_build(new_param_hash, boundary))
+ end
+
+ # Return a multipart value segment from a file upload handle.
+ def uploaded_file_segment(key, upload_io, boundary)
+ <<-EOF
+--#{boundary}\r
+Content-Disposition: form-data; name="#{key}"; filename="#{Camping.escape(upload_io.original_filename)}"\r
+Content-Type: #{upload_io.content_type}\r
+Content-Length: #{upload_io.size}\r
+\r
+#{upload_io.read}\r
+EOF
+ end
+
+ # Return a conventional value segment from a parameter value
+ def conventional_segment(key, value, boundary)
+ <<-EOF
+--#{boundary}\r
+Content-Disposition: form-data; name="#{key}"\r
+\r
+#{value}\r
+EOF
+ end
+
+ # Build a multipart request body that includes both uploaded files and conventional parameters.
+ # To have predictable results we sort the output segments (a hash passed in will not be
+ # iterated over in the original definition order anyway, as a good developer should know)
+ def multipart_build(params, boundary)
+ flat = []
+ flatten_hash(params) do | keys, value |
+ if keys[-1].nil? # warn the user that Camping will never see that
+ raise Mosquito::SageAdvice,
+ "Camping will only show you the last element of the array when using multipart forms"
+ end
+
+ flat_key = [esc(keys.shift), keys.map{|k| "[%s]" % esc(k) }].flatten.join
+ if value.respond_to?(:original_filename)
+ flat << uploaded_file_segment(flat_key, value, boundary)
+ else
+ flat << conventional_segment(flat_key, value, boundary)
+ end
+ end
+ flat.sort.join("")+"--#{boundary}--\r"
+ end
+
+ # Build a query string. The brackets are NOT encoded. Camping is peculiar in that
+ # in contrast to Rails it wants item=1&item=2 to make { item=>[1,2] } to make arrays. We have
+ # to account for that.
+ def qs_build (hash)
+ returning([]) do | qs |
+ flatten_hash(hash) do | keys, value |
+ keys.pop if keys[-1].nil? # cater for camping array handling
+ if value.respond_to?(:original_filename)
+ raise Mosquito::SageAdvice, "Sending a file using GET won't do you any good"
+ end
+
+ qs << [esc(keys.shift), keys.map{|k| "[%s]" % esc(k)}, '=', esc(value)].flatten.join
+ end
+ end.sort.join('&')
+ end
+
+ # Will accept a hash or array of any depth, collapse it into
+ # pairs in the form of ([first_level_k, second_level_k, ...], value)
+ # and yield these pairs as it goes to the supplied block. Some
+ # pairs might be yieled twice because arrays create repeating keys.
+ def flatten_hash(hash_or_a, parent_keys = [], &blk)
+ if hash_or_a.is_a?(Hash)
+ hash_or_a.each_pair do | k, v |
+ flatten_hash(v, parent_keys + [k], &blk)
+ end
+ elsif hash_or_a.is_a?(Array)
+ hash_or_a.map do | v |
+ blk.call(parent_keys + [nil], v)
+ end
+ else
+ blk.call(parent_keys, hash_or_a)
+ end
+ end
+end
+
+# Works like a wrapper for a simulated file upload. To use:
+#
+# uploaded = Mosquito::MockUpload.new("beach.jpg")
+#
+# This will create a file with the JPEG content-type and 122 bytes of purely random data, which
+# can then be submitted as a part of the test request
+class Mosquito::MockUpload < StringIO
+ attr_reader :local_path, :original_filename, :content_type, :extension
+ IMAGE_TYPES = {:jpg => 'image/jpeg', :png => 'image/png', :gif => 'image/gif',
+ :pdf => 'application/pdf' }.stringify_keys
+
+ def initialize(filename)
+ tempname = "tempfile_#{Time.now.to_i}"
+
+ @temp = ::Tempfile.new(tempname)
+ @local_path = @temp.path
+ @original_filename = File.basename(filename)
+ @extension = File.extname(@original_filename).gsub(/^\./, '').downcase
+ @content_type = IMAGE_TYPES[@extension] || "application/#{@extension}"
+
+ size = 100.bytes
+ super("Stub file %s \n%s\n" % [@original_filename, Mosquito::garbage(size)])
+ end
+
+ def inspect
+ info = " @size='#{length}' @filename='#{original_filename}' @content_type='#{content_type}'>"
+ super[0..-2] + info
+ end
+
+end
+
+# Stealing our assigns the evil way. This should pose no problem
+# for things that happen in the controller actions, but might be tricky
+# if some other service upstream munges the variables.
+# This service will always get included last (innermost), so it runs regardless of
+# the services upstream (such as HTTP auth) that might not call super
+module Mosquito::Proboscis #:nodoc:
+ def service(*a)
+ returning(super(*a)) do
+ a = instance_variables.inject({}) do | assigns, ivar |
+ assigns[ivar.gsub(/^@/, '')] = instance_variable_get(ivar); assigns
+ end
+ Mosquito.stash(::Camping::H[a])
+ end
+ end
+end
+
+module Camping
+
+ class Test < Test::Unit::TestCase
+
+ def test_dummy; end #:nodoc
+
+ # The reverse of the reverse of the reverse of assert(condition)
+ def deny(condition, message='')
+ assert !condition, message
+ end
+
+ # http://project.ioni.st/post/217#post-217
+ #
+ # def test_new_publication
+ # assert_difference(Publication, :count) do
+ # post :create, :publication_title => ...
+ # # ...
+ # end
+ # end
+ #
+ # Is the number of items different?
+ #
+ # Can be used for increment and decrement.
+ #
+ def assert_difference(object, method = :count, difference = 1)
+ initial_value = object.send(method)
+ yield
+ assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
+ end
+
+ # See +assert_difference+
+ def assert_no_difference(object, method, &block)
+ assert_difference object, method, 0, &block
+ end
+ end
+
+ # Used to test the controllers and rendering. The test should be called <App>Test
+ # (BlogTest for the aplication called Blog). A number of helper instance variables
+ # will be created for you - @request, which will contain a Mosquito::MockRequest
+ # object, @response (contains the response with headers and body), @cookies (a hash)
+ # and @state (a hash). Request and response will be reset in each test.
+ class WebTest < Test
+
+ # Gives you access to the instance variables assigned by the controller
+ attr_reader :assigns
+
+ def test_dummy; end #:nodoc
+
+ def setup
+ @class_name_abbr = self.class.name.gsub(/^Test/, '')
+ @request = Mosquito::MockRequest.new
+ @cookies, @response, @assigns = {}, {}, {}
+ end
+
+ # Send a GET request to a URL
+ def get(url='/', vars={})
+ send_request url, vars, 'GET'
+ end
+
+ # Send a POST request to a URL. All requests except GET will allow
+ # setting verbatim URL-encoded parameters as the third argument instead
+ # of a hash.
+ def post(url, post_vars={})
+ send_request url, post_vars, 'POST'
+ end
+
+ # Send a DELETE request to a URL. All requests except GET will allow
+ # setting verbatim URL-encoded parameters as the third argument instead
+ # of a hash.
+ def delete(url, vars={})
+ send_request url, vars, 'DELETE'
+ end
+
+ # Send a PUT request to a URL. All requests except GET will allow
+ # setting verbatim URL-encoded parameters as the third argument instead
+ # of a hash.
+ def put(url, vars={})
+ send_request url, vars, 'PUT'
+ end
+
+ # Send any request. We will try to guess what you meant - if there are uploads to be
+ # processed it's not going to be a GET, that's for sure.
+ def send_request(url, post_vars, method)
+
+ if method.to_s.downcase == "get"
+ @request.query_string_params = post_vars
+ else
+ @request.post_params = post_vars
+ end
+
+ # If there is some stuff in the URL to be used as a query string, why ignore it?
+ url, qs_from_url = url.split(/\?/)
+
+ relativize_url!(url)
+
+ @request.append_to_query_string(qs_from_url) if qs_from_url
+
+ # We do allow the user to override that one
+ @request['REQUEST_METHOD'] = method
+
+ @request['SCRIPT_NAME'] = '/' + @class_name_abbr.downcase
+ @request['PATH_INFO'] = '/' + url
+
+ @request['REQUEST_URI'] = [@request.SCRIPT_NAME, @request.PATH_INFO].join('').squeeze('/')
+ unless @request['QUERY_STRING'].blank?
+ @request['REQUEST_URI'] += ('?' + @request['QUERY_STRING'])
+ end
+
+ if @cookies
+ @request['HTTP_COOKIE'] = @cookies.map {|k,v| "#{k}=#{Camping.escape(v)}" }.join('; ')
+ end
+
+ # Inject the proboscis if we haven't already done so
+ pr = Mosquito::Proboscis
+ eval("#{@class_name_abbr}.send(:include, pr) unless #{@class_name_abbr}.ancestors.include?(pr)")
+
+ # Run the request
+ @response = eval("#{@class_name_abbr}.run @request.body, @request")
+ @assigns = Mosquito::unstash
+
+ # We need to restore the cookies separately so that the app
+ # restores our session on the next request. We retrieve cookies and
+ # the session in their assigned form instead of parsing the headers and
+ # doing a deserialization cycle
+ @cookies = @assigns[:cookies] || H[{}]
+ @state = @assigns[:state] || H[{}]
+
+ if @response.headers['X-Sendfile']
+ @response.body = File.read(@response.headers['X-Sendfile'])
+ end
+ end
+
+ # Assert a specific response (:success, :error or a freeform error code as integer)
+ def assert_response(status_code)
+ case status_code
+ when :success
+ assert_equal 200, @response.status
+ when :redirect
+ assert_equal 302, @response.status
+ when :error
+ assert @response.status >= 500,
+ "Response status should have been >= 500 but was #{@response.status}"
+ else
+ assert_equal status_code, @response.status
+ end
+ end
+
+ # Check that the text in the body matches a regexp
+ def assert_match_body(regex, message=nil)
+ assert_match regex, @response.body, message
+ end
+
+ # Opposite of +assert_match_body+
+ def assert_no_match_body(regex, message=nil)
+ assert_no_match regex, @response.body, message
+ end
+
+ # Make sure that we are redirected to a certain URL. It's not needed
+ # to prepend the URL with a mount (instead of "/blog/latest-news" you can use "/latest-news")
+ #
+ # Checks both the response status and the url.
+ def assert_redirected_to(url, message=nil)
+ assert_response :redirect
+ assert_equal url, extract_redirection_url, message
+ end
+
+ # Assert that a cookie of name matches a certain pattern
+ def assert_cookie(name, pat, message=nil)
+ assert_match pat, @cookies[name], message
+ end
+
+ # Nothing is new under the sun
+ def follow_redirect
+ get extract_redirection_url
+ end
+
+ # Quickly gives you a handle to a file with random content
+ def upload(filename)
+ Mosquito::MockUpload.new(filename)
+ end
+
+ # Checks that Camping sent us a cookie to attach a session
+ def assert_session_started
+ assert_not_nil @cookies["camping_sid"],
+ "The session ID cookie was empty although session should have started"
+ end
+
+ # The reverse of +assert_session_started+
+ def assert_no_session
+ assert_nil @cookies["camping_sid"],
+ "A session cookie was sent although this should not happen"
+ end
+
+ private
+ def extract_redirection_url
+ loc = @response.headers['Location']
+ path_seg = @response.headers['Location'].path.gsub(%r!/#{@class_name_abbr.downcase}!, '')
+ loc.query ? (path_seg + "?" + loc.query).to_s : path_seg.to_s
+ end
+
+ def relativize_url!(url)
+ return unless url =~ /^([a-z]+):\//
+ p = URI.parse(url)
+ unless p.host == @request.domain
+ raise ::Mosquito::NonLocalRequest,
+ "You tried to callout to #{p} which is outside of the test domain"
+ end
+ url.replace(p.path + (p.query.blank ? '' : "?#{p.query}"))
+ end
+ end
+
+ # Used to test the models - no infrastructure will be created for running the request
+ class ModelTest < Test
+ def test_dummy; end #:nodoc
+ end
+
+ # Deprecated but humane
+ UnitTest = ModelTest
+ FunctionalTest = WebTest
+end
71 public/bare.rb
@@ -0,0 +1,71 @@
+#!/usr/local/bin/ruby -rubygems
+require 'camping'
+
+Camping.goes :Bare
+
+module Bare::Controllers
+ class Index < R '/'
+ def get
+ render :index
+ end
+ end
+
+ # class SendAFile < R '/file'
+ # def get
+ # # Send this file back
+ #
+ # end
+ # end
+
+ class ThisOneWillError < R '/error'
+ def get
+ raise "An error for testing only!"
+ end
+ end
+
+ class ThisOneWillError404 < R '/error404'
+ def get
+ @status = 404
+ end
+ end
+
+ class ThisOneWillRedirect < R '/redirect'
+ def get
+ redirect R(Page, 'faq')
+ end
+ end
+
+ class Page < R '/(\w+)'
+ def get(page_name)
+ render page_name
+ end
+ end
+end
+
+module Bare::Views
+ def layout
+ html do
+ title { 'My Bare' }
+ body { self << yield }
+ end
+ end
+ def index
+ p 'Hi my name is Charles.'
+ p 'Here are some links:'
+ ul do
+ li { a 'Google', :href => 'http://google.com' }
+ li { a 'A sample page', :href => '/sample' }
+ end
+ end
+ def sample
+ p 'A sample page'
+ end
+end
+
+if __FILE__ == $0
+ require 'mongrel/camping'
+
+ server = Mongrel::Camping::start("0.0.0.0",3002,"/homepage",Bare)
+ puts "** Bare example is running at http://localhost:3002/homepage"
+ server.run.join
+end
53 public/blog.rb
@@ -0,0 +1,53 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+gem 'camping', '>=1.4'
+require 'camping'
+require 'camping/session'
+
+Camping.goes :Blog
+
+require File.dirname(__FILE__) + '/blog/models'
+require File.dirname(__FILE__) + '/blog/views'
+require File.dirname(__FILE__) + '/blog/controllers'
+
+Blog::Models.schema do
+ create_table :blog_posts, :force => true do |t|
+ t.column :id, :integer, :null => false
+ t.column :user_id, :integer, :null => false
+ t.column :title, :string, :limit => 255
+ t.column :body, :text
+ end
+ create_table :blog_users, :force => true do |t|
+ t.column :id, :integer, :null => false
+ t.column :username, :string
+ t.column :password, :string
+ end
+ create_table :blog_comments, :force => true do |t|
+ t.column :id, :integer, :null => false
+ t.column :post_id, :integer, :null => false
+ t.column :username, :string
+ t.column :body, :text
+ end
+ execute "INSERT INTO blog_users (username, password) VALUES ('admin', 'camping')"
+end
+
+def Blog.create
+ unless Blog::Models::Post.table_exists?
+ ActiveRecord::Schema.define(&Blog::Models.schema)
+ end
+end
+
+if __FILE__ == $0
+ require 'mongrel/camping'
+
+ Blog::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog.db'
+ Blog::Models::Base.logger = Logger.new('camping.log')
+ Blog::Models::Base.threaded_connections=false
+ Blog.create
+
+ server = Mongrel::Camping::start("0.0.0.0",3002,"/blog",Blog)
+ puts "** Blog example is running at http://localhost:3002/blog"
+ puts "** Default username is `admin', password is `camping'"
+ server.run.join
+end
163 public/blog/controllers.rb
@@ -0,0 +1,163 @@
+
+module Blog::Controllers
+ class Index < R '/'
+ def get
+ @posts = Post.find :all
+ render :index
+ end
+ end
+
+ class Add
+ def get
+ unless @state.user_id.blank?
+ @user = User.find @state.user_id
+ @post = Post.new
+ end
+ render :add
+ end
+ def post
+ post = Post.create({
+ :title => input.post_title,
+ :body => input.post_body,
+ :user_id => @state.user_id
+ })
+ redirect View, post
+ end
+ end
+
+ class Info < R '/info/(\d+)', '/info/(\w+)/(\d+)', '/info', '/info/(\d+)/(\d+)/(\d+)/([\w-]+)'
+ def get(*args)
+ div do
+ code args.inspect; br; br
+ code ENV.inspect; br
+ code "Link: #{R(Info, 1, 2)}"
+ end
+ end
+ end
+
+ class View < R '/view/(\d+)'
+ def get post_id
+ @post = Post.find post_id
+ @comments = Models::Comment.find_all_by_post_id post_id
+ render :view
+ end
+ end
+
+ class Edit < R '/edit/(\d+)', '/edit'
+ def get post_id
+ unless @state.user_id.blank?
+ @user = User.find @state.user_id
+ end
+ @post = Post.find post_id
+ render :edit
+ end
+
+ def post
+ @post = Post.find input.post_id
+ @post.update_attributes :title => input.post_title, :body => input.post_body
+ redirect View, @post
+ end
+ end
+
+ class Comment
+ def post
+ Models::Comment.create({
+ :username => input.post_username,
+ :body => input.post_body,
+ :post_id => input.post_id
+ })
+ redirect View, input.post_id
+ end
+ end
+
+ class Login
+ def post
+ @user = User.find(:first, {
+ :conditions => [
+ 'username = ? AND password = ?',
+ input.username,
+ input.password
+ ]
+ })
+
+ if @user
+ @login = 'login success !'
+ @state.user_id = @user.id
+ else
+ @login = 'wrong user name or password'
+ end
+ render :login
+ end
+ end
+
+ class Cookies < R '/cookies'
+ def get
+ @cookies.awesome_cookie = 'camping for good'
+ @state.awesome_data = 'camping for good'
+ @posts = Post.find(:all)
+ render :index
+ end
+ end
+
+ class Logout
+ def get
+ @state.user_id = nil
+ render :logout
+ end
+ end
+
+ class Style < R '/styles.css'
+ def get
+ @headers["Content-Type"] = "text/css; charset=utf-8"
+ @body = %{
+ body {
+ font-family: Utopia, Georgia, serif;
+ }
+ h1.header {
+ background-color: #fef;
+ margin: 0; padding: 10px;
+ }
+ div.content {
+ padding: 10px;
+ }
+ }
+ end
+ end
+
+ # The following is introduced as a means to quickly test roundtrips
+ class SessionRoundtrip < R('/session-roundtrip')
+ def get
+ @state[:flag_in_session] = "This is a flag"
+ end
+
+ def post
+ if @state[:flag_in_session]
+ @state[:second_flag] = "This is a second flag"
+ end
+ return ''
+ end
+ end
+
+ class Redirector < R('/redirector')
+ def get
+ redirect '/blog/sniffer?one=two'
+ end
+ end
+
+ class Sniffer < R('/sniffer')
+ def get
+ input.to_hash.to_yaml
+ end
+ alias_method :post, :get
+ end
+
+ class Restafarian < R('/rest')
+ def delete
+ return "Called delete"
+ end
+
+ def put
+ return "Called put"
+ end
+ end
+end
11 public/blog/models.rb
@@ -0,0 +1,11 @@
+
+module Blog::Models
+ def self.schema(&block)
+ @@schema = block if block_given?
+ @@schema
+ end
+
+ class Post < Base; belongs_to :user; end
+ class Comment < Base; belongs_to :user; end
+ class User < Base; validates_presence_of :username; end
+end
113 public/blog/views.rb
@@ -0,0 +1,113 @@
+
+module Blog::Views
+
+ def layout
+ html do
+ head do
+ title 'blog'
+ link :rel => 'stylesheet', :type => 'text/css', :href => '/styles.css', :media => 'screen'
+ end
+ body do
+ h1.header { a 'blog', :href => R(Index) }
+ div.content do
+ self << yield
+ end
+ end
+ end
+ end
+
+ def index
+ if @posts.empty?
+ p 'No posts found.'
+ p { a 'Add', :href => R(Add) }
+ else
+ for post in @posts
+ _post(post)
+ end
+ end
+ end
+
+ def login
+ p { b @login }
+ p { a 'Continue', :href => R(Add) }
+ end
+
+ def logout
+ p "You have been logged out."
+ p { a 'Continue', :href => R(Index) }
+ end
+
+ def add
+ if @user
+ _form(post, :action => R(Add))
+ else
+ _login
+ end
+ end
+
+ def edit
+ if @user
+ _form(post, :action => R(Edit))
+ else
+ _login
+ end
+ end
+
+ def view
+ _post(post)
+
+ p "Comment for this post:"
+ for c in @comments
+ h1 c.username
+ p c.body
+ end
+
+ form :action => R(Comment), :method => 'post' do
+ label 'Name', :for => 'post_username'; br
+ input :name => 'post_username', :type => 'text'; br
+ label 'Comment', :for => 'post_body'; br
+ textarea :name => 'post_body' do; end; br
+ input :type => 'hidden', :name => 'post_id', :value => post.id
+ input :type => 'submit'
+ end
+ end
+
+ # partials
+ def _login
+ form :action => R(Login), :method => 'post' do
+ label 'Username', :for => 'username'; br
+ input :name => 'username', :type => 'text'; br
+
+ label 'Password', :for => 'password'; br
+ input :name => 'password', :type => 'text'; br
+
+ input :type => 'submit', :name => 'login', :value => 'Login'
+ end
+ end
+
+ def _post(post)
+ h1 post.title
+ p post.body
+ p do
+ a "Edit", :href => R(Edit, post)
+ a "View", :href => R(View, post)
+ end
+ end
+
+ def _form(post, opts)
+ p do
+ text "You are logged in as #{@user.username} | "
+ a 'Logout', :href => R(Logout)
+ end
+ form({:method => 'post'}.merge(opts)) do
+ label 'Title', :for => 'post_title'; br
+ input :name => 'post_title', :type => 'text', :value => post.title; br
+
+ label 'Body', :for => 'post_body'; br
+ textarea post.body, :name => 'post_body'; br
+
+ input :type => 'hidden', :name => 'post_id', :value => post.id
+ input :type => 'submit'
+ end
+ end
+ end
6 test/fixtures/blog_comments.yml
@@ -0,0 +1,6 @@
+
+fox:
+ id: 1
+ post_id: 1
+ username: phil
+ body: That is a boring story.
11 test/fixtures/blog_posts.yml
@@ -0,0 +1,11 @@
+quick_fox:
+ id: 1
+ user_id: 1
+ title: News for Today
+ body: The quick fox jumped over the lazy dog.
+
+quick_dog:
+ id: 2
+ user_id: 2
+ title: News for Tomorrow
+ body: The quick dog jumped over the lazy fox.
6 test/fixtures/blog_users.yml
@@ -0,0 +1,6 @@
+
+quentin:
+ id: 1
+ username: quentin
+ password: password
+
25 test/sage_advice_cases/parsing_arrays.rb
@@ -0,0 +1,25 @@
+require 'rubygems'
+require 'camping'
+
+Camping.goes :ParsingArrays
+
+class ParsingArrays::Controllers::Klonk < ParsingArrays::Controllers::R('/')
+ def get; render :foam; end
+ def post; input.inspect; end
+end
+
+module ParsingArrays::Views
+ def foam
+ h2 "This is multipart with arrays"
+ form(:method => :post, :enctype => 'multipart/form-data') { _inputs }
+ h2 "This is urlencoded with arrays"
+ form(:method => :post, :enctype => 'application/x-www-form-urlencoded') { _inputs }
+ end
+
+ def _inputs
+ input :type => :text, :name => "array", :value => '1'
+ input :type => :text, :name => "array", :value => '2'
+ input :type => :text, :name => "array", :value => '3'
+ input :type => :submit, :name => 'flush', :value => 'Observe'
+ end
+end
66 test/test_bare.rb
@@ -0,0 +1,66 @@
+require File.dirname(__FILE__) + "/../lib/mosquito"
+require File.dirname(__FILE__) + "/../public/bare"
+
+# When you got a few Camping apps in one process you actually install the session
+# for all of them simply by including. We want to test operation without sessions so we
+# call this app Bare, which comes before Blog.
+class TestBare < Camping::FunctionalTest
+
+ test "should get index with success" do
+ get '/'
+ assert_response :success
+ assert_no_session
+ assert_match_body %r!Charles!
+ end
+
+ def test_get_without_arguments_should_give_us_the_index_page
+ get
+ assert_response :success
+ assert_match_body %r!Charles!
+ end
+
+ test "should get page with success" do
+ get '/sample'
+ assert_response :success
+ assert_no_session
+ assert_match_body %r!<p>A sample page</p>!
+ end
+
+ def test_request_uri_preserves_query_vars
+ get '/sample', :somevar => 10
+ assert_equal '/bare/sample?somevar=10', @request['REQUEST_URI']
+ end
+
+ test "should assert_no_match_body" do
+ get '/sample'
+ assert_no_match_body /Rubber\s+Bubblegum\s+Burt Reynolds\s+Hippopotamus/
+ end
+
+ test "should return error" do
+ get '/error'
+ assert_response :error
+ end
+
+ test "should return 404 error" do
+ get '/error404'
+ assert_response 404
+ end
+
+ def test_assigning_verbatim_post_payload
+ post '/sample', 'foo=bar&plain=flat'
+ @request.body.rewind
+ assert_equal 'foo=bar&plain=flat', @request.body.read
+ end
+
+ test "should redirect" do
+ get '/redirect'
+ assert_redirected_to '/faq'
+ end
+
+ # test "should send file" do
+ # get '/file'
+ # assert_response :success
+ # # TODO
+ # end
+
+end
233 test/test_blog.rb
@@ -0,0 +1,233 @@
+require File.dirname(__FILE__) + "/../lib/mosquito"
+require File.dirname(__FILE__) + "/../public/blog"
+Blog.create
+include Blog::Models
+
+class TestBlog < Camping::FunctionalTest
+ fixtures :blog_posts, :blog_users, :blog_comments
+
+ def setup
+ super
+ # We inject the session into the blog here to prevent it from attaching to Bare as well. Normally this
+ # should not happen but we need to take sides to test for sessioneless Camping compliance.
+ unless @sesion_set
+ Blog.send(:include, Camping::Session)
+ ActiveRecord::Migration.suppress_messages do
+ ::Camping::Models::Session.create_schema
+ end
+ @sesion_set = true
+ end
+ end
+
+ def test_cookies
+ get '/cookies'
+ assert_cookie 'awesome_cookie', 'camping for good'
+ assert_equal @state.awesome_data, 'camping for good'
+ get '/'
+ assert_equal @state.awesome_data, 'camping for good'
+ end
+
+ def test_cookies_persisted_across_requests_and_escaping_properly_handled
+ @cookies["asgård"] = 'Wøbble'
+ get '/cookies'
+ assert_equal 'asgård=W%C3%B8bble', @request['HTTP_COOKIE'], "The cookie val shouldbe escaped"
+ assert_response :success
+ assert_equal 'Wøbble', @cookies["asgård"]
+
+ get '/'
+ assert_equal @state.awesome_data, 'camping for good'
+ assert_equal 'Wøbble', @cookies["asgård"]
+ end
+
+ def test_index
+ get
+ assert_response :success
+ assert_match_body %r!>blog<!
+ assert_not_equal @state.awesome_data, 'camping for good'
+ assert_kind_of Array, @assigns[:posts]
+ assert_kind_of Post, assigns[:posts].first
+ end
+
+ def test_view
+ get '/view/1'
+ assert_response :success
+ assert_match_body %r!The quick fox jumped over the lazy dog!
+ end
+
+ def test_styles
+ get 'styles.css'
+ assert_match_body %r!Utopia!
+ end
+
+ def test_edit_should_require_login
+ get '/edit/1'
+ assert_response :success
+ assert_match_body 'login'
+ end
+
+ def test_login
+ post 'login', :username => 'quentin', :password => 'password'
+ assert_match_body 'login success'
+ end
+
+ def test_comment
+ assert_difference(Comment) {
+ post 'comment', {
+ :post_username => 'jim',
+ :post_body => 'Nice article.',
+ :post_id => 1
+ }
+ assert_response :redirect
+ assert_redirected_to '/view/1'
+ }
+ end
+
+ def test_sage_advice_raised_when_getting_with_files
+ assert_raise(Mosquito::SageAdvice) do
+ get '/view/1', :afile => Mosquito::MockUpload.new("apic.jpg")
+ end
+ end
+
+ def test_session_roundtrip_across_successive_requests
+ get '/session-roundtrip'
+ assert @state.has_key?(:flag_in_session)
+
+ assert_session_started
+ post '/session-roundtrip'
+ assert @state.has_key?(:second_flag), "The :second_flag key in the session gets set only if the previous flag was present"
+ assert_session_started
+ end
+
+ def test_request_uri_has_no_double_slashes
+ get '/session-roundtrip'
+ assert_equal "/blog/session-roundtrip", @request['REQUEST_URI']
+ end
+
+ def test_follow_redirect
+ get '/redirector'
+ assert_response :redirect
+
+ assert_redirected_to '/sniffer?one=two'
+
+ follow_redirect
+ roundtipped_params = YAML::load(StringIO.new(@response.body))
+ ref = {"one" => "two"}.with_indifferent_access
+ assert_equal ref, roundtipped_params
+ end
+
+ def test_request_honors_verbatim_query_string_and_passed_params
+ assert_nothing_raised do
+ get '/sniffer?one=2&foo=baz', :taing => 44
+ end
+
+ roundtripped = YAML::load(StringIO.new(@response.body.to_s))
+ ref = {"taing"=>"44", "one" => "2", "foo" => "baz"}
+ assert_equal ref, roundtripped
+ end
+
+ def test_uplaod_gets_a_quick_uplaod_handle
+ file = upload("pic.jpg")
+ assert_kind_of Mosquito::MockUpload, file
+ end
+
+ def test_intrinsic_methods
+ # This WILL use mocks when we get to it from more pressing matters
+ delete '/rest'
+ assert_equal 'Called delete', @response.body
+
+ put '/rest'
+ assert_equal 'Called put', @response.body
+ end
+
+ def calling_with_an_absolute_url_should_relativize
+ assert_equal 'test.host', @request.domain
+ put 'http://test.host/blog/rest'
+ assert_nothing_raised do
+ assert_equal 'Called put', @response.body
+ end
+ end
+
+ def test_calling_with_an_absolute_url_outside_of_the_default_test_host_must_raise
+ assert_equal 'test.host', @request.domain
+ assert_raise(Mosquito::NonLocalRequest) do
+ put 'http://yahoo.com/blog/rest'
+ end
+ end
+
+ def test_calling_with_an_absolute_url_outside_of_the_custom_test_host_must_raise
+ @request.domain = 'foo.bar'
+ assert_raise(Mosquito::NonLocalRequest) do
+ put 'http://test.host/blog/rest'
+ end
+ end
+
+end
+
+class TestPost < Camping::ModelTest
+
+ fixtures :blog_posts, :blog_users, :blog_comments
+
+ def test_fixtures_path_is_relative_to_the_testcase
+ assert_equal 'test/fixtures/', self.class.fixture_path
+ end
+
+ def test_create
+ post = create
+ assert post.valid?
+ end
+
+ def test_assoc
+ post = Post.find :first
+ assert_kind_of User, post.user
+ assert_equal 1, post.user.id
+ end
+
+ def test_destroy
+ original_count = Post.count
+ Post.destroy 1
+ assert_equal original_count - 1, Post.count
+ end
+
+ private
+
+ def create(options={})
+ Post.create({
+ :user_id => 1,
+ :title => "Title",
+ :body => "Body"
+ }.merge(options))
+ end
+
+end
+
+class TestUser < Camping::ModelTest
+
+ fixtures :blog_posts, :blog_users, :blog_comments
+
+ def test_create
+ user = create
+ assert user.valid?
+ end
+
+ def test_required
+ user = create(:username => nil)
+ deny user.valid?
+ assert_not_nil user.errors.on(:username)
+ end
+
+ test "should require username" do
+ assert_no_difference(User, :count) do
+ User.create(:username => nil)
+ end
+ end
+
+ private
+
+ def create(options={})
+ User.create({
+ :username => 'godfrey',
+ :password => 'password'
+ }.merge(options))
+ end
+
+end
20 test/test_helpers.rb
@@ -0,0 +1,20 @@
+require File.dirname(__FILE__) + "/../lib/mosquito"
+
+class TestHelpers < Camping::ModelTest
+ # http://rubyforge.org/tracker/index.php?func=detail&aid=8921&group_id=351&atid=1416
+ def test_supports_old_style_and_new_style_fixture_generation
+ assert self.respond_to?(:create_fixtures), "Oldstyle method should work"
+ assert self.class.respond_to?(:fixtures), "Newstyle method should work"
+ end
+
+ def test_stash_and_unstash
+ someval = {:foo => "bar"}
+ assert_nothing_raised do
+ assert_equal someval, Mosquito::stash(someval)
+ end
+
+ retr = Mosquito.unstash
+ assert_nil Mosquito.unstash, "There is nothing stashed now"
+ assert_equal retr, someval, "The value should be retrieved"
+ end
+end
292 test/test_mock_request.rb
@@ -0,0 +1,292 @@
+require File.dirname(__FILE__) + "/../lib/mosquito"
+
+$KCODE = 'u'
+# Sadly, Camping does not mandate UTF-8 - but we will use it here to torment the
+# URL-escaping routines wïs ze ümlåuts.
+
+# We use Sniffer to check if our request is parsed properly. After being called Sniffer
+# will raise a Messenger exception with the @input inside.
+Camping.goes :Sniffer
+class Sniffer::Controllers::ParamPeeker < Sniffer::Controllers::R('/')
+ class Messenger < RuntimeError
+ attr_reader :packet
+ def initialize(packet)
+ @packet = packet
+ end
+ end
+
+ def post
+ raise Messenger.new(input.dup)
+ end
+ alias_method :get, :post
+end
+
+class Sniffer::Controllers::ServerError
+ def get(*a)
+ raise a.pop
+ end
+end
+
+class TestMockRequest < Test::Unit::TestCase
+ include Mosquito
+
+ def setup
+ @parsed_input = nil
+ @req = MockRequest.new
+ end
+
+ def test_default_domain_and_port
+ assert_equal 'test.host', @req.to_hash['SERVER_NAME']
+ assert_equal 'test.host', @req.to_hash['HTTP_HOST']
+ end
+
+ def test_envars_translate_to_readers
+ %w( server_name path_info accept_encoding user_agent
+ script_name server_protocol cache_control accept_language
+ host remote_addr server_software keep_alive referer accept_charset
+ version request_uri server_port gateway_interface accept connection
+ request_method).map do | envar |
+ true_value = (@req[envar.upcase] || @req["HTTP_" + envar.upcase])
+ assert_not_nil true_value,
+ "The environment of the default request should provide #{envar.upcase} or HTTP_#{envar.upcase}"
+ assert_nothing_raised do
+ assert_equal true_value, @req.send(envar), "The request should understand the reader for #{envar}"
+ assert_equal true_value, @req.send(envar.upcase), "The request should understand the reader for #{envar.upcase} " +
+ "for backwards compatibility"
+ end
+ end
+ end
+
+ def test_accessor_for_query_string
+ assert @req.respond_to?(:query_string), "The request must have a reader for query string"
+ assert @req.respond_to?(:query_string=), "The request must have a writer for query string"
+
+ qs = "one=two&foo=bar"
+ assert_nothing_raised { @req.query_string = qs }
+ assert_equal "?one=two&foo=bar", @req.query_string,
+ "The request should return the query string segment with a question mark"
+
+ qs = "?one=two&foo=bar"
+ assert_equal "?one=two&foo=bar", @req.query_string,
+ "The request should return the query string segment with one and only one question mark"
+
+ assert_equal '/?one=two&foo=bar', @req['REQUEST_URI'],
+ "The assigned query string should be propargated to REQUEST_URI"
+
+ assert_equal 'one=two&foo=bar', @req['QUERY_STRING'],
+ "The assigned query string should be propargated to QUERY_STRING"
+
+ @req.query_string = ''
+ assert_equal '/', @req['REQUEST_URI'], "The query part should be removed from the REQUEST_URI along with the qmark"
+ end
+
+ def test_query_string_composition_from_params
+ composable_hash = {
+ :foo => "bar",
+ :user => {
+ :login => "boss",
+ :password => "secret",
+ :friend_ids => [1, 2, 3]
+ }
+ }
+
+ assert @req.respond_to?(:query_string_params=)
+ @req.query_string_params = composable_hash
+
+ ref = '?foo=bar&user[friend_ids]=1&user[friend_ids]=2&user[friend_ids]=3&user[login]=boss&user[password]=secret'
+ assert_equal normalize_qs(ref), normalize_qs(@req.query_string)
+ assert_equal "/"+ref, @req['REQUEST_URI'], "The query string parameters should be propagated to the " +
+ "request uri and be in their sorted form"
+ assert_equal normalize_qs(ref), normalize_qs(@req["QUERY_STRING"]), "The query string should also land in QUERY_STRING"
+ end
+
+ def test_qs_assignment_with_empty_hash_unsets_envar
+ @req.query_string_params = {}
+ assert_equal '', @req.query_string, "When an empty hash is assigned the query string should be empty"
+ assert_nil @req.to_hash['QUERY_STRING'], "The key for QUERY_STRING should be unset in the environment"
+ end
+
+ def test_multipart_boundary_generation
+ boundaries = (1..40).map do
+ @req.generate_boundary
+ end
+ assert_equal boundaries.uniq.length, boundaries.length, "All boundaries generated should be unique"
+ assert_equal boundaries, boundaries.grep(/^msqto\-/), "All boundaries should be prepended with msqto-"
+ end
+
+ def test_method_missing_is_indeed_missing
+ assert_raise(NoMethodError) { @req.kaboodle! }
+ end
+
+ def test_extract_values
+ t = {
+ :foo => "bar",
+ :bar => {
+ :baz => [1,2,3],
+ :bam => "trunk",
+ },
+ :sequence => "xyz"
+ }
+ assert_equal ["bar", "1", "2" , "3", "trunk", "xyz"].sort, @req.send(:extract_values, t).map(&:to_s).sort,
+ "should properly extract infinitely deeply nested values"
+ end
+
+ def test_post_composition_with_urlencoding
+ assert_equal 'GET', @req['REQUEST_METHOD'], "The default request method is GET"
+
+ @req.post_params = {:hello => "welæcome", :name => "john", :data => {:values => [1,2,3] } }
+ assert_kind_of StringIO, @req.body, "The request body is an IO"
+ assert_equal 'POST', @req['REQUEST_METHOD'],
+ "When the parameters are assigned the request should be switched to POST"
+ assert_equal "application/x-www-form-urlencoded", @req['CONTENT_TYPE'],
+ "The content-type should be switched accordingly"
+ assert_equal normalize_qs('data[values]=1&data[values]=2&data[values]=3&hello=wel%C3%A6come&name=john'), normalize_qs(@req.body.read),
+ "The body should now contain URL-encoded form parameters"
+ end
+
+ def test_post_composition_accepts_verbatim_strings_as_payload
+ assert_equal 'GET', @req['REQUEST_METHOD'], "The default request method is GET"
+ @req.post_params = 'foo=bar&baz=bad'
+ assert_equal 'POST', @req['REQUEST_METHOD']
+ assert_equal "application/x-www-form-urlencoded", @req['CONTENT_TYPE'],
+ "The content-type should be switched accordingly"
+ assert_equal 'foo=bar&baz=bad', @req.body.read, "The payload should have been assigned directly"
+ end
+
+ def test_append_to_query_string
+ @req.append_to_query_string "x=y&boo=2"
+ @req.append_to_query_string "zaz=taing&schmoo=tweed"
+ assert_equal "x=y&boo=2&zaz=taing&schmoo=tweed", @req["QUERY_STRING"]
+ end
+
+ # We could let that one slip but why? If someone does TDD he deserves gratification
+ def test_post_composition_requiring_multipart_with_arrays_warns_the_noble_developer_and_everyone_stays_happy
+ assert_raise(Mosquito::SageAdvice) do
+ @req.post_params = {:hello => "welcome", :name => "john", :arrayed => [1, 2, 3], :somefile => MockUpload.new("pic.jpg") }
+ end
+
+ assert_nothing_raised do
+ @req.post_params = {:hello => "welcome", :name => "john", :arrayed => [1, 2, 3], :not_a_file => "shtaink" }
+ end
+ end
+
+ # We could let that one slip but why? If someone does TDD he deserves gratification
+ def test_get_composition_with_files_warns_the_noble_developer_and_he_quickly_corrects_himself
+ assert_raise(Mosquito::SageAdvice) do
+ @req.query_string_params = {:hello => "welcome", :somefile => MockUpload.new("pic.jpg") }
+ end
+ end
+
+ def test_post_composition_from_values_requiring_multipart
+ assert_equal 'GET', @req['REQUEST_METHOD'], "The default request method is GET"
+
+ @req.post_params = {:hello => "welcome", :name => "john", :somefile => MockUpload.new("pic.jpg") }
+
+ assert_kind_of StringIO, @req.body, "The request body is an IO"
+ assert_equal 'POST', @req['REQUEST_METHOD'], "When the parameters are assigned the request should be switched to POST"
+ ctype, boundary = @req['CONTENT_TYPE'].split(/=/)
+ boundary_with_prefix = "--" + boundary
+ assert_equal "multipart/form-data; boundary", ctype, "The content-type should be switched accordingly"
+
+ @req.body.rewind
+ output = @req.body.read.split("\r")
+ ref_segments = [
+ "--#{boundary}",
+ "\nContent-Disposition: form-data; name=\"hello\"",
+ "\n", "\nwelcome", "\n--#{boundary}",
+ "\nContent-Disposition: form-data; name=\"name\"",
+ "\n",
+ "\njohn",
+ "\n--#{boundary}",
+ "\nContent-Disposition: form-data; name=\"somefile\"; filename=\"pic.jpg\"",
+ "\nContent-Type: image/jpeg", "\nContent-Length: 120",
+ "\n",
+ /\nStub file pic\.jpg \n([AZ-az]{120})\n/,
+ "\n--#{boundary}--"
+ ]
+
+ ref_segments.each_with_index do | ref, idx |
+ if ref == String
+ assert_equal ref, output[idx], "The segment #{idx} should be #{ref}"
+ elsif ref == Regexp
+ assert_match ref, output[idx], "The segment #{idx} should match #{ref}"
+ end
+ end
+ end
+
+ private
+ # Remove the question mark, sort the pairs
+ def normalize_qs(query_string)
+ "?" + query_string.to_s.gsub(/^\?/, '').split(/&/).sort.join('&')
+ end
+end
+
+
+class TestMockRequestWithRoundtrip < Test::Unit::TestCase
+ include Mosquito
+ def setup
+ @parsed_input = nil
+ @req = MockRequest.new
+ end
+
+ def test_multipart_post_properly_roundtripped
+ @req.post_params = {:hello => "welcome", :name => "john", :somefile => MockUpload.new("pic.jpg") }
+
+ run_request!
+
+ # Reference is something like this
+ # {"name"=>"john", "somefile"=>{"name"=>"somefile", "type"=>"image/jpeg",
+ # "tempfile"=>#<File:/tmp/C.9366.0>, "filename"=>"pic.jpg"}, "hello"=>"welcome"}
+
+ assert_equal "john", @parsed_input["name"]
+ assert_kind_of Hash, @parsed_input["somefile"]
+ assert_equal "somefile", @parsed_input["somefile"]["name"]
+ assert_equal "pic.jpg", @parsed_input["somefile"]["filename"]
+ assert_equal "image/jpeg", @parsed_input["somefile"]["type"]
+ assert_kind_of Tempfile, @parsed_input["somefile"]["tempfile"]
+
+
+ @req.post_params = {:hello => "welcome", :name => "john", :arrayed => [1, 2, 3], :somefile => "instead" }
+ assert_kind_of StringIO, @req.body, "The request body is an IO"
+ assert_match /urlencoded/, @req['CONTENT_TYPE'], "The ctype should have been switched back to urlencoded"
+
+ run_request!
+
+ ref = {"name"=>"john", "arrayed"=>["1", "2", "3"], "somefile"=>"instead", "hello"=>"welcome"}
+ assert_equal ref, @parsed_input, "Camping should have parsed our params just like so"
+ end
+
+ def test_reader_and_writer_for_domain
+ assert_nothing_raised { assert_equal 'test.host', @req.domain }
+ assert_nothing_raised { @req.domain = "foo.dzing" }
+
+ assert_equal 'foo.dzing', @req.domain
+ assert_equal 'foo.dzing', @req['SERVER_NAME']
+ assert_equal 'foo.dzing', @req['HTTP_HOST']
+ end
+
+ def test_query_string_properly_roundtripped
+ parsed = {"user"=> {"friend_äidéis"=>["1", "2", "3"], "paßwort"=>"secret", "login"=>"boss"}, "foo"=>"bar"}
+ @req.query_string_params = parsed
+ run_request!
+ assert_equal parsed, @parsed_input
+ end
+
+ def test_urlencoded_post_properly_roundtripped
+ assert_equal 'GET', @req['REQUEST_METHOD'], "The default request method is GET"
+
+ @req.post_params = {:hello => "welæcome", :name => "john", :data => {:values => [1,2,3] } }
+ run_request!
+ ref = {"name"=>"john", "hello"=>"welæcome", "data"=>{"values"=>["1", "2", "3"]}}
+ assert_equal ref, @parsed_input, "Camping should have parsed our input like so"
+ end
+ private
+ def run_request!
+ @req.body.rewind
+ begin
+ Sniffer.run(@req.body, @req.to_hash)
+ rescue Sniffer::Controllers::ParamPeeker::Messenger => e
+ @parsed_input = e.packet
+ end
+ end
+end
55 test/test_mock_upload.rb
@@ -0,0 +1,55 @@
+require File.dirname(__FILE__) + "/../lib/mosquito"
+
+class TestMockUpload < Test::Unit::TestCase
+ include Mosquito
+
+ def setup
+ @mock_jpeg = MockUpload.new("stuff.JPG")
+ @mock_png = MockUpload.new("stuff.png")
+ @mock_gif = MockUpload.new("stuff.gif")
+ @mock_pdf = MockUpload.new("doc.pdf")
+ end
+
+ def test_generation
+ [@mock_jpeg, @mock_png, @mock_gif].each do | obj |
+ assert_kind_of StringIO, obj
+ assert obj.respond_to?(:content_type)
+ assert obj.respond_to?(:local_path)
+ assert obj.respond_to?(:original_filename)
+ assert File.exist?(obj.local_path)
+ end
+ end
+
+ def test_extensions_extracted_properly
+ assert_equal 'jpg', @mock_jpeg.extension
+ assert_equal 'png', @mock_png.extension
+ assert_equal 'gif', @mock_gif.extension
+ assert_equal 'pdf', @mock_pdf.extension
+ end
+
+ def test_content_types_detected_properly
+ assert_equal 'image/jpeg', @mock_jpeg.content_type
+ assert_equal 'image/png', @mock_png.content_type
+ assert_equal 'image/gif', @mock_gif.content_type
+ assert_equal 'application/pdf', @mock_pdf.content_type
+ end
+
+ def test_inspekt
+ desc = @mock_png.inspect
+ assert desc.include?("@content_type='image/png'")
+ end
+
+ def test_original_filenames_overridden_properly
+ assert_equal "stuff.JPG", @mock_jpeg.original_filename
+ assert_equal "stuff.png", @mock_png.original_filename
+ assert_equal "stuff.gif", @mock_gif.original_filename
+ assert_equal "doc.pdf", @mock_pdf.original_filename
+ end
+
+ def test_proper_garbage_put_into_files
+ garbage_bins = [@mock_jpeg, @mock_png, @mock_gif].map{|u| u.read }
+ garbage_bins.map do | chunk |
+ assert_equal 122, chunk.size, "Should be this amount of random data"
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.