tarantula-rails3 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. data/CHANGELOG +49 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +161 -0
  4. data/Rakefile +83 -0
  5. data/VERSION.yml +4 -0
  6. data/examples/example_helper.rb +57 -0
  7. data/examples/relevance/core_extensions/ellipsize_example.rb +19 -0
  8. data/examples/relevance/core_extensions/file_example.rb +8 -0
  9. data/examples/relevance/core_extensions/response_example.rb +29 -0
  10. data/examples/relevance/core_extensions/test_case_example.rb +20 -0
  11. data/examples/relevance/tarantula/attack_handler_example.rb +29 -0
  12. data/examples/relevance/tarantula/basic_attack_example.rb +12 -0
  13. data/examples/relevance/tarantula/crawler_example.rb +375 -0
  14. data/examples/relevance/tarantula/form_example.rb +50 -0
  15. data/examples/relevance/tarantula/form_submission_example.rb +171 -0
  16. data/examples/relevance/tarantula/html_document_handler_example.rb +43 -0
  17. data/examples/relevance/tarantula/html_report_helper_example.rb +46 -0
  18. data/examples/relevance/tarantula/html_reporter_example.rb +82 -0
  19. data/examples/relevance/tarantula/invalid_html_handler_example.rb +33 -0
  20. data/examples/relevance/tarantula/io_reporter_example.rb +11 -0
  21. data/examples/relevance/tarantula/link_example.rb +84 -0
  22. data/examples/relevance/tarantula/log_grabber_example.rb +26 -0
  23. data/examples/relevance/tarantula/rails_integration_proxy_example.rb +88 -0
  24. data/examples/relevance/tarantula/result_example.rb +85 -0
  25. data/examples/relevance/tarantula/tidy_handler_example.rb +58 -0
  26. data/examples/relevance/tarantula/transform_example.rb +20 -0
  27. data/examples/relevance/tarantula_example.rb +23 -0
  28. data/laf/images/header_bg.jpg +0 -0
  29. data/laf/images/logo.png +0 -0
  30. data/laf/images/tagline.png +0 -0
  31. data/laf/javascripts/jquery-1.2.3.js +3408 -0
  32. data/laf/javascripts/jquery-ui-tabs.js +890 -0
  33. data/laf/javascripts/jquery.tablesorter.js +861 -0
  34. data/laf/javascripts/tarantula.js +10 -0
  35. data/laf/stylesheets/tarantula.css +346 -0
  36. data/lib/relevance/core_extensions/ellipsize.rb +34 -0
  37. data/lib/relevance/core_extensions/file.rb +9 -0
  38. data/lib/relevance/core_extensions/metaclass.rb +78 -0
  39. data/lib/relevance/core_extensions/response.rb +9 -0
  40. data/lib/relevance/core_extensions/string_chars_fix.rb +11 -0
  41. data/lib/relevance/core_extensions/test_case.rb +19 -0
  42. data/lib/relevance/tarantula.rb +58 -0
  43. data/lib/relevance/tarantula/attack.rb +18 -0
  44. data/lib/relevance/tarantula/attack_handler.rb +37 -0
  45. data/lib/relevance/tarantula/basic_attack.rb +40 -0
  46. data/lib/relevance/tarantula/crawler.rb +254 -0
  47. data/lib/relevance/tarantula/detail.html.erb +81 -0
  48. data/lib/relevance/tarantula/form.rb +23 -0
  49. data/lib/relevance/tarantula/form_submission.rb +88 -0
  50. data/lib/relevance/tarantula/html_document_handler.rb +36 -0
  51. data/lib/relevance/tarantula/html_report_helper.rb +39 -0
  52. data/lib/relevance/tarantula/html_reporter.rb +105 -0
  53. data/lib/relevance/tarantula/index.html.erb +37 -0
  54. data/lib/relevance/tarantula/invalid_html_handler.rb +18 -0
  55. data/lib/relevance/tarantula/io_reporter.rb +34 -0
  56. data/lib/relevance/tarantula/link.rb +94 -0
  57. data/lib/relevance/tarantula/log_grabber.rb +16 -0
  58. data/lib/relevance/tarantula/rails_integration_proxy.rb +68 -0
  59. data/lib/relevance/tarantula/recording.rb +12 -0
  60. data/lib/relevance/tarantula/response.rb +13 -0
  61. data/lib/relevance/tarantula/result.rb +77 -0
  62. data/lib/relevance/tarantula/test_report.html.erb +32 -0
  63. data/lib/relevance/tarantula/tidy_handler.rb +32 -0
  64. data/lib/relevance/tarantula/transform.rb +17 -0
  65. data/lib/relevance/tasks/tarantula_tasks.rake +42 -0
  66. data/lib/tarantula-rails3.rb +9 -0
  67. data/template/tarantula_test.rb +22 -0
  68. metadata +164 -0
@@ -0,0 +1,94 @@
1
+ class Relevance::Tarantula::Link
2
+ include Relevance::Tarantula
3
+
4
+ class << self
5
+ include ActionView::Helpers::UrlHelper
6
+ # method_javascript_function needs this method
7
+ def protect_against_forgery?
8
+ false
9
+ end
10
+ #fast fix for rails3
11
+ def method_javascript_function(method, url = '', href = nil)
12
+ action = (href && url.size > 0) ? "'#{url}'" : 'this.href'
13
+ submit_function =
14
+ "var f = document.createElement('form'); f.style.display = 'none'; " +
15
+ "this.parentNode.appendChild(f); f.method = 'POST'; f.action = #{action};"
16
+
17
+ unless method == :post
18
+ submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); "
19
+ submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);"
20
+ end
21
+
22
+ if protect_against_forgery?
23
+ submit_function << "var s = document.createElement('input'); s.setAttribute('type', 'hidden'); "
24
+ submit_function << "s.setAttribute('name', '#{request_forgery_protection_token}'); s.setAttribute('value', '#{escape_javascript form_authenticity_token}'); f.appendChild(s);"
25
+ end
26
+ submit_function << "f.submit();"
27
+ end
28
+ end
29
+
30
+ METHOD_REGEXPS = {}
31
+ [:put, :delete, :post].each do |m|
32
+ # remove submit from the end so we'll match with or without forgery protection
33
+ s = method_javascript_function(m).gsub( /f.submit();/, "" )
34
+ # don't just match this.href in case a different url was passed originally
35
+ s = Regexp.escape(s).gsub( /this.href/, ".*" )
36
+ METHOD_REGEXPS[m] = /#{s}/
37
+ end
38
+
39
+ attr_accessor :href, :crawler, :referrer
40
+
41
+ def initialize(link, crawler, referrer)
42
+ @crawler, @referrer = crawler, referrer
43
+
44
+ if String === link || link.nil?
45
+ @href = transform_url(link)
46
+ @method = :get
47
+ else # should be a tag
48
+ @href = link['href'] ? transform_url(link['href'].downcase) : nil
49
+ @tag = link
50
+ end
51
+ end
52
+
53
+ def crawl
54
+ response = crawler.follow(method, href)
55
+ log "Response #{response.code} for #{self}"
56
+ crawler.handle_link_results(self, make_result(response))
57
+ end
58
+
59
+ def make_result(response)
60
+ crawler.make_result(:method => method,
61
+ :url => href,
62
+ :response => response,
63
+ :referrer => referrer)
64
+ end
65
+
66
+ def method
67
+ @method ||= begin
68
+ (@tag &&
69
+ [:put, :delete, :post].detect do |m| # post should be last since it's least specific
70
+ @tag['onclick'] =~ METHOD_REGEXPS[m]
71
+ end) ||
72
+ :get
73
+ end
74
+ end
75
+
76
+ def transform_url(link)
77
+ crawler.transform_url(link)
78
+ end
79
+
80
+ def ==(obj)
81
+ obj.respond_to?(:href) && obj.respond_to?(:method) &&
82
+ self.href.to_s == obj.href.to_s && self.method.to_s == obj.method.to_s
83
+ end
84
+ alias :eql? :==
85
+
86
+ def hash
87
+ to_s.hash
88
+ end
89
+
90
+ def to_s
91
+ "<Relevance::Tarantula::Link href=#{href}, method=#{method}>"
92
+ end
93
+
94
+ end
@@ -0,0 +1,16 @@
1
+ class Relevance::Tarantula::LogGrabber
2
+ attr_accessor :path
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ def clear!
8
+ File.open(@path, "w")
9
+ end
10
+
11
+ def grab!
12
+ File.read(@path)
13
+ ensure
14
+ clear!
15
+ end
16
+ end
@@ -0,0 +1,68 @@
1
+ require 'test/unit'
2
+
3
+ class Relevance::Tarantula::RailsIntegrationProxy
4
+ include Relevance::Tarantula
5
+ extend Relevance::Tarantula
6
+ extend Forwardable
7
+ attr_accessor :integration_test
8
+
9
+ def self.rails_integration_test(integration_test, options = {})
10
+ t = Crawler.new
11
+ t.max_url_length = options[:max_url_length] if options[:max_url_length]
12
+ t.proxy = RailsIntegrationProxy.new(integration_test)
13
+ t.handlers << HtmlDocumentHandler.new(t)
14
+ t.handlers << InvalidHtmlHandler.new
15
+ t.log_grabber = Relevance::Tarantula::LogGrabber.new(File.join(rails_root, "log/test.log"))
16
+ t.skip_uri_patterns << /logout$/
17
+ t.transform_url_patterns += [
18
+ [/\?\d+$/, ''], # strip trailing numbers for assets
19
+ [/^http:\/\/#{integration_test.host}/, ''] # strip full path down to relative
20
+ ]
21
+ t.test_name = t.proxy.integration_test.method_name
22
+ t.reporters << Relevance::Tarantula::HtmlReporter.new(t.report_dir)
23
+ t
24
+ end
25
+
26
+ def initialize(integration_test)
27
+ @integration_test = integration_test
28
+ @integration_test.meta.attr_accessor :response
29
+ end
30
+
31
+ [:get, :post, :put, :delete].each do |verb|
32
+ define_method(verb) do |url, *args|
33
+ integration_test.send(verb, url, *args)
34
+ response = integration_test.response
35
+ patch_response(url, response)
36
+ response
37
+ end
38
+ end
39
+
40
+ def patch_response(url, response)
41
+ if response.code == '404'
42
+ if File.exist?(static_content_path(url))
43
+ case ext = File.extension(url)
44
+ when /html|te?xt|css|js|jpe?g|gif|psd|png|eps|pdf|ico/
45
+ response.body = static_content_file(url)
46
+ response.headers["type"] = "text/#{ext}" # readable as response.content_type
47
+ response.meta.attr_accessor :code
48
+ response.code = "200"
49
+ else
50
+ log "Skipping unknown type #{url}"
51
+ end
52
+ end
53
+ end
54
+ # don't count on metaclass taking block, e.g.
55
+ # http://relevancellc.com/2008/2/12/how-should-metaclass-work
56
+ response.metaclass.class_eval do
57
+ include Relevance::CoreExtensions::Response
58
+ end
59
+ end
60
+
61
+ def static_content_file(url)
62
+ File.read(static_content_path(url))
63
+ end
64
+
65
+ def static_content_path(url)
66
+ File.expand_path(File.join(rails_root, "public", url))
67
+ end
68
+ end
@@ -0,0 +1,12 @@
1
+ module Recording
2
+ def self.stderr
3
+ $stderr = recorder = StringIO.new
4
+ begin
5
+ yield
6
+ ensure
7
+ $stderr = STDERR
8
+ end
9
+ recorder.rewind
10
+ recorder.read
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # Used to create a stub response when we didn't get back a real response
2
+ class Relevance::Tarantula::Response
3
+ HASHABLE_ATTRS = [:code, :body, :content_type]
4
+ attr_accessor *HASHABLE_ATTRS
5
+
6
+ def initialize(hash)
7
+ hash.each do |k,v|
8
+ raise ArgumentError, k unless HASHABLE_ATTRS.member?(k)
9
+ self.instance_variable_set("@#{k}", v)
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,77 @@
1
+ class Relevance::Tarantula::Result
2
+ HASHABLE_ATTRS = [:success, :method, :url, :response, :referrer, :data, :description, :log, :test_name]
3
+ DEFAULT_LOCALHOST = "http://localhost:3000"
4
+ attr_accessor *HASHABLE_ATTRS
5
+ include Relevance::Tarantula
6
+ include Relevance::Tarantula::HtmlReportHelper
7
+
8
+ def initialize(hash)
9
+ hash.each do |k,v|
10
+ raise ArgumentError, k unless HASHABLE_ATTRS.member?(k)
11
+ self.instance_variable_set("@#{k}", v)
12
+ end
13
+ end
14
+
15
+ def short_description
16
+ [method,url].join(" ")
17
+ end
18
+
19
+ def sequence_number
20
+ @sequence_number ||= (self.class.next_number += 1)
21
+ end
22
+
23
+ def file_name
24
+ "#{sequence_number}.html"
25
+ end
26
+
27
+ def code
28
+ response && response.code
29
+ end
30
+
31
+ def body
32
+ response && response.body
33
+ end
34
+
35
+ def full_url
36
+ "#{DEFAULT_LOCALHOST}#{url}"
37
+ end
38
+
39
+ ALLOW_NNN_FOR = /^allow_(\d\d\d)_for$/
40
+
41
+ class << self
42
+ attr_accessor :next_number
43
+
44
+ def handle(result)
45
+ retval = result.dup
46
+ retval.success = successful?(result.response) || can_skip_error?(result)
47
+ retval.description = "Bad HTTP Response" unless retval.success
48
+ retval
49
+ end
50
+
51
+ def success_codes
52
+ %w{200 201 302 401}
53
+ end
54
+
55
+ # allow_errors_for is a hash
56
+ # k=error code,
57
+ # v=array of matchers for urls that can skip said error
58
+ attr_accessor :allow_errors_for
59
+ def can_skip_error?(result)
60
+ coll = allow_errors_for[result.code]
61
+ return false unless coll
62
+ coll.any? {|item| item === result.url}
63
+ end
64
+
65
+ def successful?(response)
66
+ success_codes.member?(response.code)
67
+ end
68
+
69
+ def method_missing(meth, *args)
70
+ super unless ALLOW_NNN_FOR =~ meth.to_s
71
+ (allow_errors_for[$1] ||= []).push(*args)
72
+ end
73
+ end
74
+
75
+ self.allow_errors_for = {}
76
+ self.next_number = 0
77
+ end
@@ -0,0 +1,32 @@
1
+ <div id="<%= test_name %>">
2
+ <% %w{failures successes}.each do |result_type| %>
3
+ <table class="list tablesorter" cellspacing="0">
4
+ <caption><%= send(result_type).size %> <%= result_type.capitalize %></caption>
5
+ <thead>
6
+ <tr>
7
+ <th class="sort asc"><span>URL</span><span class="sort">&nbsp;</span></th>
8
+ <th><span>Action</span><span class="sort">&nbsp;</span></th>
9
+ <th><span>Response</span><span class="sort">&nbsp;</span></th>
10
+ <th class="left"><span>Description</span><span class="sort">&nbsp;</span></th>
11
+ <th><span>Referrer</span><span class="sort">&nbsp;</span></th>
12
+ </tr>
13
+ </thead>
14
+ <tfoot>
15
+ <tr><td colspan="5">&nbsp;</td></tr>
16
+ </tfoot>
17
+
18
+ <tbody>
19
+ <% send(result_type).sort{|x,y| y.code.to_s <=> x.code.to_s}.each_with_index do |result,i| %>
20
+ <tr class="<%= (i%2 == 0) ? 'even' : 'odd' %>">
21
+ <td class="left"><a href="<%= "#{test_name}/#{result.file_name}" %>"><%= result.url.ellipsize(50) %></a></td>
22
+ <td class="method"><%= result.method.to_s.upcase %></td> <!-- TODO Clean up demeter violation -->
23
+ <td><span class="<%= class_for_code(result.code) %>"><%= result.code %></span></td>
24
+ <td class="left"><%= result.description %></td>
25
+ <td class="left"><%= result.referrer.ellipsize(30) %></td>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
30
+ <br/>
31
+ <% end %>
32
+ </div>
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ begin
3
+ gem 'tidy'
4
+ require 'tidy'
5
+ rescue Gem::LoadError
6
+ puts "Tidy gem not available -- 'gem install tidy' to get it."
7
+ end
8
+
9
+ if defined? Tidy
10
+ Tidy.path = ENV['TIDY_PATH'] if ENV['TIDY_PATH']
11
+
12
+ class Relevance::Tarantula::TidyHandler
13
+ include Relevance::Tarantula
14
+ def initialize(options = {})
15
+ @options = {:show_warnings=>true}.merge(options)
16
+ end
17
+ def handle(result)
18
+ response = result.response
19
+ return unless response.html?
20
+ tidy = Tidy.open(@options) do |tidy|
21
+ xml = tidy.clean(response.body)
22
+ tidy
23
+ end
24
+ unless tidy.errors.blank?
25
+ error_result = result.dup
26
+ error_result.description = "Bad HTML (Tidy)"
27
+ error_result.data = tidy.errors.inspect
28
+ error_result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ class Relevance::Tarantula::Transform
2
+ attr_accessor :from, :to
3
+ def initialize(from, to)
4
+ @from = from
5
+ @to = to
6
+ end
7
+ def [](string)
8
+ case to
9
+ when Proc
10
+ string.gsub(from, &to)
11
+ else
12
+ string.gsub(from, to)
13
+ end
14
+ end
15
+ end
16
+
17
+
@@ -0,0 +1,42 @@
1
+ require 'rake'
2
+
3
+ namespace :tarantula do
4
+
5
+ desc 'Run tarantula tests.'
6
+ task :test do
7
+ rm_rf "tmp/tarantula"
8
+ Rake::TestTask.new(:tarantula_test) do |t|
9
+ t.libs << 'test'
10
+ t.pattern = 'test/tarantula/**/*_test.rb'
11
+ t.verbose = true
12
+ end
13
+
14
+ Rake::Task[:tarantula_test].invoke
15
+ end
16
+
17
+ desc 'Run tarantula tests and open results in your browser.'
18
+ task :report do
19
+ begin
20
+ Rake::Task['tarantula:test'].invoke
21
+ rescue RuntimeError => e
22
+ puts e.message
23
+ end
24
+
25
+ Dir.glob("tmp/tarantula/**/index.html") do |file|
26
+ if PLATFORM['darwin']
27
+ system("open #{file}")
28
+ elsif PLATFORM[/linux/]
29
+ system("firefox #{file}")
30
+ else
31
+ puts "You can view tarantula results at #{file}"
32
+ end
33
+ end
34
+ end
35
+
36
+ desc 'Generate a default tarantula test'
37
+ task :setup do
38
+ mkdir_p "test/tarantula"
39
+ template_path = File.expand_path(File.join(File.dirname(__FILE__), "../../..", "template", "tarantula_test.rb"))
40
+ cp template_path, "test/tarantula/"
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ module Relevance
2
+ module Tarantula
3
+ class Railtie < ::Rails::Railtie
4
+ rake_tasks do
5
+ load "relevance/tasks/tarantula_tasks.rake"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ require "test_helper"
2
+ require "relevance/tarantula"
3
+
4
+ class TarantulaTest < ActionController::IntegrationTest
5
+ # Load enough test data to ensure that there's a link to every page in your
6
+ # application. Doing so allows Tarantula to follow those links and crawl
7
+ # every page. For many applications, you can load a decent data set by
8
+ # loading all fixtures.
9
+ fixtures :all
10
+
11
+ def test_tarantula
12
+ # If your application requires users to log in before accessing certain
13
+ # pages, uncomment the lines below and update them to allow this test to
14
+ # log in to your application. Doing so allows Tarantula to crawl the
15
+ # pages that are only accessible to logged-in users.
16
+ #
17
+ # post '/session', :login => 'quentin', :password => 'monkey'
18
+ # follow_redirect!
19
+
20
+ tarantula_crawl(self)
21
+ end
22
+ end