Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

adding simplecov for code coverage, added view helper test, some furt…

…her clean ups
  • Loading branch information...
commit d694d3407e224fcec19b05081c814df298d11089 1 parent 9aabf4d
Matthew Hutchinson authored
4 Gemfile
View
@@ -1,4 +1,2 @@
-source "http://rubygems.org"
-
-# Specify your gem's dependencies in acts_as_textcaptcha.gemspec
+source 'http://rubygems.org'
gemspec
4 LICENSE
View
@@ -1,4 +1,4 @@
-Copyright (c) 2010 Matthew Hutchinson
+Copyright (c) 2011 Matthew Hutchinson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ 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.
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 Rakefile
View
@@ -14,14 +14,11 @@ Rake::TestTask.new do |t|
end
# code coverage
-namespace :cover_me do
- desc "Generates and opens code coverage report."
- task :report do
- require 'cover_me'
- CoverMe.config.project.root = File.expand_path('../', __FILE__)
- CoverMe.config.file_pattern = [/.*\.rb/i]
- Rake::Task['test'].invoke
- CoverMe.complete!
+namespace :test do
+ desc "Run all tests and generate a code coverage report (simplecov)"
+ task :coverage do
+ ENV['COVERAGE'] = 'true'
+ Rake::Task['test'].execute
end
end
13 acts_as_textcaptcha.gemspec
View
@@ -9,24 +9,23 @@ Gem::Specification.new do |s|
s.authors = ["Matthew Hutchinson"]
s.email = ["matt@hiddenloop.com"]
s.homepage = "http://github.com/matthutchinson/acts_as_textcaptcha"
- s.summary = %q{Spam protection for your models via logic questions and the excellent textcaptcha.com api}
- s.description = %q{Spam protection for your ActiveRecord models using logic questions and the excellent textcaptcha api. See textcaptcha.com for more details and to get your api key.
- The logic questions are aimed at a child's age of 7, so can be solved easily by all but the most cognitively impaired users. As they involve human logic, such questions cannot be solved by a robot.
- For more reasons on why logic questions are useful, see here; http://textcaptcha.com/why}
+ s.summary = %q{Spam protection for your models via logic questions and the textcaptcha.com API}
+ s.description = %q{Simple question/answer based spam protection for your Rails models.
+ You can define your own logic questions and/or fetch questions from the textcaptcha.com API.
+ The questions involve human logic and are tough for spam bots to crack.
+ For more reasons on why logic questions are a good idea visit; http://textcaptcha.com/why}
s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test}/*`.split("\n")
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.add_dependency('bcrypt-ruby', '~> 2.1.4')
s.add_development_dependency('rails')
- s.add_development_dependency('activerecord')
s.add_development_dependency('bundler')
- s.add_development_dependency('cover_me')
+ s.add_development_dependency('simplecov')
s.add_development_dependency('rdoc')
s.add_development_dependency('sqlite3')
s.add_development_dependency('fakeweb')
4 init.rb
View
@@ -1,2 +1,2 @@
-# init.rb, not included in gemspec files, but required to work as a rails plugin
-require './vendor/plugins/acts_as_textcaptcha/lib/acts_as_textcaptcha'
+# this file is required to allow the gem to work as a rails plugin
+require './vendor/plugins/acts_as_textcaptcha/lib/acts_as_textcaptcha'
32 lib/acts_as_textcaptcha/textcaptcha.rb
View
@@ -1,7 +1,6 @@
require 'yaml'
require 'net/http'
require 'digest/md5'
-require 'logger'
# compatiblity when XmlMini is not available
require 'xml' unless defined?(ActiveSupport::XmlMini)
@@ -27,11 +26,10 @@ class Railtie < ::Rails::Railtie
module Textcaptcha #:nodoc:
- # This exception is raised if you an empty response is returned from the web service
+ # raised if an empty response is ever returned from textcaptcha.com web service
class BadResponse < StandardError; end;
def acts_as_textcaptcha(options = nil)
-
cattr_accessor :textcaptcha_config
attr_accessor :spam_question, :spam_answers, :spam_answer
attr_protected :spam_question if respond_to?(:accessible_attributes) && accessible_attributes.nil?
@@ -43,8 +41,8 @@ def acts_as_textcaptcha(options = nil)
else
begin
self.textcaptcha_config = YAML.load(File.read("#{Rails.root ? Rails.root.to_s : '.'}/config/textcaptcha.yml"))[Rails.env].symbolize_keys!
- rescue Errno::ENOENT
- raise 'ActsAsTextcaptcha >> could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file'
+ rescue
+ raise 'could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file'
end
end
@@ -54,24 +52,24 @@ def acts_as_textcaptcha(options = nil)
module InstanceMethods
- # override this method to toggle spam checking, default is on (true)
- def perform_textcaptcha?; true end
+ # override this method to toggle textcaptcha spam checking, default is on (true)
+ def perform_textcaptcha?
+ true
+ end
+ # generate textcaptcha question and encrypt possible spam_answers
def textcaptcha
return if !perform_textcaptcha? || validate_spam_answer
-
- # always clear answer before generating a new question
self.spam_answer = nil
if textcaptcha_config
unless BCrypt::Engine.valid_salt?(textcaptcha_config[:bcrypt_salt])
- raise BCrypt::Errors::InvalidSalt.new "ActsAsTextcaptcha >> you must specify a valid BCrypt Salt in your acts_as_textcaptcha options, get a salt from irb/console with\nrequire 'bcrypt';BCrypt::Engine.generate_salt\n\n(Please check Gem README for more details)\n"
+ raise BCrypt::Errors::InvalidSalt.new "you must specify a valid BCrypt Salt in your acts_as_textcaptcha options, get a salt from irb/console with\nrequire 'bcrypt';BCrypt::Engine.generate_salt\n\n(Please check Gem README for more details)\n"
end
if textcaptcha_config[:api_key]
begin
- # URI.parse is deprecated in 1.9.2
- uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
- response = Net::HTTP.get(uri_parser.parse("http://textcaptcha.com/api/#{textcaptcha_config[:api_key]}"))
+ uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI # URI.parse is deprecated in 1.9.2
+ response = Net::HTTP.get(uri_parser.parse("http://textcaptcha.com/api/#{textcaptcha_config[:api_key]}"))
if response.empty?
raise Textcaptcha::BadResponse
else
@@ -81,6 +79,7 @@ def textcaptcha
rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, URI::InvalidURIError,
REXML::ParseException, Textcaptcha::BadResponse
+ # rescue from these errors and continue
end
end
@@ -90,7 +89,7 @@ def textcaptcha
self.spam_question = random_question[:question]
self.spam_answers = encrypt_answers(random_question[:answers].split(',').map!{ |answer| md5_answer(answer) })
else
- self.spam_question = 'ActsAsTextcaptcha, No API key set (or captcha questions configured) and/or the textcaptcha service is currently unavailable (type ok to bypass)'
+ self.spam_question = 'ActsAsTextcaptcha >> no API key (or questions) set and/or the textcaptcha service is currently unavailable (answer ok to bypass)'
self.spam_answers = 'ok'
end
end
@@ -104,7 +103,7 @@ def parse_textcaptcha_xml(xml)
parsed_xml = ActiveSupport::XmlMini.parse(xml)['captcha']
self.spam_question = parsed_xml['question']['__content__']
if parsed_xml['answer'].is_a?(Array)
- self.spam_answers = encrypt_answers(parsed_xml['answer'].collect {|a| a['__content__']})
+ self.spam_answers = encrypt_answers(parsed_xml['answer'].collect { |a| a['__content__'] })
else
self.spam_answers = encrypt_answers([parsed_xml['answer']['__content__']])
end
@@ -120,10 +119,11 @@ def validate_spam_answer
end
def validate_textcaptcha
- # if not new_record? we dont spam check on existing records (ie. no spam check on updates/edits)
+ # only spam check on new/unsaved records (ie. no spam check on updates/edits)
if !respond_to?('new_record?') || new_record?
if perform_textcaptcha? && !validate_spam_answer
errors.add(:spam_answer, :incorrect_answer, :message => "is incorrect, try another question instead")
+ # regenerate question
textcaptcha
return false
end
4 lib/acts_as_textcaptcha/textcaptcha_helper.rb
View
@@ -1,7 +1,7 @@
module ActsAsTextcaptcha
module TextcaptchaHelper
- # builds html fields for spam question, answer and hidden encrypted answers
+ # builds html fields for spam question, answer and hidden encrypted spam answers
def textcaptcha_fields(f, &block)
model = f.object
captcha_html = ''
@@ -13,6 +13,8 @@ def textcaptcha_fields(f, &block)
captcha_html += capture(&block)
end
end
+
+ # Rails 2 compatability
if Rails::VERSION::MAJOR < 3
concat captcha_html, &block.binding
else
17 lib/tasks/textcaptcha.rake
View
@@ -1,27 +1,26 @@
require 'bcrypt'
namespace :textcaptcha do
+
desc "Creates a template config file in config/textcaptcha.yml"
task :config do
-
src = File.join(File.dirname(__FILE__), '../..', 'config', 'textcaptcha.yml')
dest = File.join(Rails.root, 'config', 'textcaptcha.yml')
if File.exist?(dest)
puts "\nOoops, a textcaptcha config file at #{dest} already exists ... aborting.\n\n"
else
- config_file = ''
- salt = BCrypt::Engine.generate_salt
+ config = ''
+ salt = BCrypt::Engine.generate_salt
f = File.open(src, 'r')
- f.each_line { |line| config_file += line }
- config_file.gsub!(/RAKE_GENERATED_SALT_PLACEHOLDER/, salt)
- config_file.gsub!(/ api_key:(.*)# for gem test purposes only$/, " api_key: PASTE_YOUR_TEXTCAPCHA_API_KEY_HERE")
- config_file.gsub!(/ bcrypt_salt:(.*)# for gem test purposes only$/, " bcrypt_salt: #{salt}")
+ f.each_line { |line| config += line }
+ config.gsub!(/RAKE_GENERATED_SALT_PLACEHOLDER/, salt)
+ config.gsub!(/ api_key:(.*)# for gem test purposes only$/, " api_key: PASTE_YOUR_TEXTCAPCHA_API_KEY_HERE")
+ config.gsub!(/ bcrypt_salt:(.*)# for gem test purposes only$/, " bcrypt_salt: #{salt}")
f = File.new(dest, 'w')
- f.write(config_file)
+ f.write(config)
f.close
puts "\ntextcaptcha.yml generated at #{dest} (with a new BCrypt salt)\nNOTE: edit this file and add your textcaptcha api key, grab one from http://textcaptcha.com/api\n\n"
end
-
end
end
21 test/database.yml
View
@@ -1,21 +0,0 @@
-sqlite:
- :adapter: sqlite
- :database: acts_as_textcaptcha.sqlite.db
-
-sqlite3:
- :adapter: sqlite3
- :database: acts_as_textcaptcha.sqlite3.db
-
-postgresql:
- :adapter: postgresql
- :username: postgres
- :password: postgres
- :database: acts_as_textcaptcha_test
- :min_messages: ERROR
-
-mysql:
- :adapter: mysql
- :host: localhost
- :username: root
- :password:
- :database: acts_as_textcaptcha_test
16 test/test_helper.rb
View
@@ -2,16 +2,26 @@
ENV['RAILS_ENV'] = 'test'
+if ENV['COVERAGE']
+ require "simplecov"
+ SimpleCov.start do
+ add_filter '/test/'
+ end
+ SimpleCov.at_exit do
+ SimpleCov.result.format!
+ `open ./coverage/index.html` if RUBY_PLATFORM =~ /darwin/
+ end
+end
+
require 'minitest/autorun'
require 'fakeweb'
require 'active_record'
require 'rails'
-require 'bcrypt'
require 'acts_as_textcaptcha'
-
+require './test/test_models'
# load and initialize test db schema
-ActiveRecord::Base.establish_connection(YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))[ENV['DB'] || 'sqlite3'])
+ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'acts_as_textcaptcha.sqlite3.db')
load(File.dirname(__FILE__) + "/schema.rb")
9 test/test_models.rb
View
@@ -1,4 +1,4 @@
-# example models configured for tests
+# models for use in tests
class Widget < ActiveRecord::Base
# uses textcaptcha.yml file for configuration
@@ -16,12 +16,12 @@ class Review < ActiveRecord::Base
acts_as_textcaptcha 'api_key' => '8u5ixtdnq9csc84cok0owswgo',
'bcrypt_salt' => '$2a$10$j0bmycH.SVfD1b5mpEGPpe',
'bcrypt_cost' => '3',
- 'questions' => [{'question' => 'The green hat is what color?', 'answers' => 'green'}]
+ 'questions' => [{ 'question' => 'The green hat is what color?', 'answers' => 'green' }]
end
class Note < ActiveRecord::Base
# inline options (string keys) with user defined questions only (no textcaptcha service)
- acts_as_textcaptcha 'questions' => [{'question' => '1+1', 'answers' => '2,two'}],
+ acts_as_textcaptcha 'questions' => [{ 'question' => '1+1', 'answers' => '2,two' }],
'bcrypt_salt' => '$2a$10$j0bmycH.SVfD1b5mpEGPpe'
# allows toggling perform_textcaptcha on/off (default on)
@@ -37,7 +37,6 @@ class Contact
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActsAsTextcaptcha::Textcaptcha
- acts_as_textcaptcha :questions => [{:question => 'one+1', :answers => '2,two'}],
+ acts_as_textcaptcha :questions => [{ :question => 'one+1', :answers => '2,two' }],
:bcrypt_salt => '$2a$10$j0bmycH.SVfD1b5mpEGPpe'
end
-
50 test/textcaptcha_helper_test.rb
View
@@ -0,0 +1,50 @@
+require File.expand_path(File.dirname(__FILE__)+'/test_helper')
+require 'action_controller'
+require 'action_view'
+
+class NotesController < ActionController::Base; end
+
+class Template < ActionView::Base
+ def protect_against_forgery?; false; end
+end
+
+
+describe 'TextcaptchaHelper' do
+
+ before(:each) do
+ @controller = NotesController.new
+ @note = Note.new
+ @note.textcaptcha
+ end
+
+ def render_template(assigns)
+ template = <<-ERB
+ <%= form_for(@note, :url => '/') do |f| %>
+ <%= textcaptcha_fields(f) do %>
+ <div class="field textcaptcha">
+ <%= f.label :spam_answer, @note.spam_question %><br/>
+ <%= f.text_field :spam_answer, :value => '' %>
+ </div>
+ <% end %>
+ <% end %>
+ ERB
+
+ Template.new([], assigns, @controller).render(:inline => template)
+ end
+
+ it 'should render question and answer fields, with hidden spam_answers field' do
+ html = render_template({:note => @note})
+
+ html.must_match /\<label for\=\"note\_spam\_answer\"\>1\+1\<\/label\>/
+ html.must_match /\<input id\=\"note_spam_answers\" name\=\"note\[spam\_answers\]\" type\=\"hidden\" value\=\"(.*)\" \/\>/
+ end
+
+ it 'should render hidden answer and spam_answer fields when question has been answered OK (and not ask question)' do
+ @note.spam_answer = 2
+ html = render_template({:note => @note})
+
+ html.wont_match /\<label for\=\"note\_spam\_answer\"\>1\+1\<\/label\>/
+ html.must_match /\<input id\=\"note_spam_answers\" name\=\"note\[spam\_answers\]\" type\=\"hidden\" value\=\"(.*)\" \/\>/
+ html.must_match /\<input id\=\"note_spam_answer\" name\=\"note\[spam_answer\]\" type\=\"hidden\" value\=\"2\" \/\>/
+ end
+end
45 test/textcaptcha_test.rb
View
@@ -1,5 +1,4 @@
-require_relative 'test_helper'
-require_relative 'test_models'
+require File.expand_path(File.dirname(__FILE__)+'/test_helper')
describe 'Textcaptcha' do
@@ -49,7 +48,7 @@
@note.save.must_equal true
end
- it "should validate a non ActiveRecord object" do
+ it 'should validate a non ActiveRecord object' do
@contact = Contact.new
@contact.textcaptcha
@@ -65,14 +64,23 @@
describe 'encryption' do
- it 'should encrypt spam_answers (joined by - seperator) MD5 digested and using BCrypt engine with salt' do
+ before(:each) do
@note = Note.new
+ end
+
+ it 'should encrypt spam_answers (joined by - seperator) MD5 digested and using BCrypt engine with salt' do
@note.spam_answers.must_be_nil
@note.textcaptcha
encrypted_answers = [2,' TwO '].collect { |answer| BCrypt::Engine.hash_secret(Digest::MD5.hexdigest(answer.to_s.strip.downcase), '$2a$10$j0bmycH.SVfD1b5mpEGPpe', 1) }.join('-')
- @note.spam_answers.must_equal("$2a$10$j0bmycH.SVfD1b5mpEGPpePFe1wBxOn7Brr9lMuLRxv6lg4ZYjJ22-$2a$10$j0bmycH.SVfD1b5mpEGPpe8v5mqqpDaExuS/hZu8Xkq8krYL/T8P.")
+ @note.spam_answers.must_equal('$2a$10$j0bmycH.SVfD1b5mpEGPpePFe1wBxOn7Brr9lMuLRxv6lg4ZYjJ22-$2a$10$j0bmycH.SVfD1b5mpEGPpe8v5mqqpDaExuS/hZu8Xkq8krYL/T8P.')
@note.spam_answers.must_equal(encrypted_answers)
end
+
+ it 'should raise error if bcyrpt salt is invalid' do
+ @note.textcaptcha_config[:bcrypt_salt] = 'bad salt'
+ proc { @note.textcaptcha }.must_raise BCrypt::Errors::InvalidSalt
+ @note.textcaptcha_config[:bcrypt_salt] ='$2a$10$j0bmycH.SVfD1b5mpEGPpe'
+ end
end
describe 'textcaptcha API' do
@@ -94,6 +102,29 @@
@review.errors[:spam_answer].first.must_equal('is incorrect, try another question instead')
end
+ it 'should parse a single answer from XML response' do
+ @review = Review.new
+ question = 'If tomorrow is Saturday, what day is today?'
+ body = "<captcha><question>#{question}</question><answer>f6f7fec07f372b7bd5eb196bbca0f3f4</answer></captcha>"
+ FakeWeb.register_uri(:get, %r|http://textcaptcha\.com/api/|, :body => body)
+
+ @review.textcaptcha
+ @review.spam_question.must_equal(question)
+ @review.spam_answers.must_equal('$2a$10$j0bmycH.SVfD1b5mpEGPpecvhlumIBvWXI4HQWk0xa74DebZDx772')
+ @review.spam_answers.split('-').length.must_equal(1)
+ end
+
+ it 'should parse multiple answers from XML response' do
+ @review = Review.new
+ question = 'If tomorrow is Saturday, what day is today?'
+ body = "<captcha><question>#{question}</question><answer>1</answer><answer>2</answer><answer>3</answer></captcha>"
+ FakeWeb.register_uri(:get, %r|http://textcaptcha\.com/api/|, :body => body)
+
+ @review.textcaptcha
+ @review.spam_question.must_equal(question)
+ @review.spam_answers.split('-').length.must_equal(3)
+ end
+
describe 'service is unavailable' do
describe 'should fallback to a user defined question' do
@@ -133,7 +164,7 @@
FakeWeb.register_uri(:get, %r|http://textcaptcha\.com/api/|, :exception => SocketError)
@comment.textcaptcha
- @comment.spam_question.must_equal 'ActsAsTextcaptcha, No API key set (or captcha questions configured) and/or the textcaptcha service is currently unavailable (type ok to bypass)'
+ @comment.spam_question.must_equal 'ActsAsTextcaptcha >> no API key (or questions) set and/or the textcaptcha service is currently unavailable (answer ok to bypass)'
@comment.spam_answers.must_equal 'ok'
end
end
@@ -144,7 +175,7 @@
Review.textcaptcha_config.must_equal({ :api_key => '8u5ixtdnq9csc84cok0owswgo',
:bcrypt_salt => '$2a$10$j0bmycH.SVfD1b5mpEGPpe',
:bcrypt_cost => '3',
- :questions => [{'question' => 'The green hat is what color?', 'answers' => 'green' }]})
+ :questions => [{ 'question' => 'The green hat is what color?', 'answers' => 'green' }]})
end
it 'should be configured with textcaptcha.yml' do
Please sign in to comment.
Something went wrong with that request. Please try again.