sinatra-security 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -19,3 +19,5 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+ /doc
23
+ /.yardoc
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.2.0
@@ -2,6 +2,8 @@ require 'sinatra/base'
2
2
 
3
3
  module Sinatra
4
4
  module Security
5
+ VERSION = "0.2.0"
6
+
5
7
  autoload :Helpers, 'sinatra/security/helpers'
6
8
  autoload :User, 'sinatra/security/user'
7
9
  autoload :Validations, 'sinatra/security/validations'
@@ -12,12 +14,43 @@ module Sinatra
12
14
  def self.registered(app)
13
15
  app.helpers Helpers
14
16
 
17
+ app.set :login_error_message, "Wrong Email and/or Password combination."
18
+ app.set :login_url, "/login"
19
+ app.set :login_user_class, :User
20
+ app.set :ignored_by_return_to, /(jpe?g|png|gif|css|js)$/
21
+
15
22
  app.post '/login' do
16
23
  if authenticate(params)
17
- redirect_to_stored
24
+ redirect_to_return_url
18
25
  else
19
- session[:error] = "Wrong Username/Email and Password combination."
20
- redirect '/login'
26
+ session[:error] = settings.login_error_message
27
+ redirect settings.login_url
28
+ end
29
+ end
30
+ end
31
+
32
+ # Allows you to declaratively declare secured locations based on their
33
+ # path prefix.
34
+ #
35
+ # @example
36
+ #
37
+ # require_login '/admin'
38
+ #
39
+ # get '/admin/posts' do
40
+ # # Posts here
41
+ # end
42
+ #
43
+ # get '/admin/users' do
44
+ # # Users here
45
+ # end
46
+ #
47
+ # @param [#to_s] path_prefix a string to match again the start of
48
+ # request.fullpath
49
+ # @return [nil]
50
+ def require_login(path_prefix)
51
+ before do
52
+ if request.fullpath =~ /^#{path_prefix}/
53
+ require_login
21
54
  end
22
55
  end
23
56
  end
@@ -1,51 +1,132 @@
1
1
  module Sinatra
2
2
  module Security
3
3
  module Helpers
4
- def redirect_to_stored
5
- if return_to = session[:return_to]
6
- session[:return_to] = nil
7
- redirect return_to
8
- else
9
- redirect "/"
10
- end
11
- end
4
+ # The main gateway. This method will redirect if no user is currently
5
+ # authenticated.
6
+ #
7
+ # @example
8
+ #
9
+ # get '/secured' do
10
+ # require_login
11
+ #
12
+ # # do super private thing here
13
+ # end
14
+ #
15
+ # @param [String] login_url (defaults to /login) the url of the login form.
16
+ # take not that even if you specify a different login form,
17
+ # the POST action for that form should still be '/login'.
18
+ def require_login(login_url = settings.login_url)
19
+ return if logged_in?
12
20
 
13
- def authenticate(params)
14
- if user = ::User.authenticate(params[:username], params[:password])
15
- session[:user] = user.id
21
+ if should_return_to?(request.fullpath)
22
+ session[:return_to] = request.fullpath
16
23
  end
24
+ redirect login_url
17
25
  end
18
-
19
- def require_login
20
- if logged_in?
21
- return true
22
- else
23
- if should_return_to?(request.fullpath)
24
- session[:return_to] = request.fullpath
25
- end
26
- redirect "/login"
27
- return false
28
- end
26
+
27
+ # Dynamic redirection based on the return path that was set.
28
+ #
29
+ # @example
30
+ #
31
+ # # By default assumes you use :return_to and '/'.
32
+ # # You can use this in your code as well. i.e.
33
+ # get '/fb/login' do
34
+ # session[:fb_return_to] = params[:from]
35
+ # # redirect to fb OAuth URI here.
36
+ # end
37
+ #
38
+ # get '/fb/success' do
39
+ # # successfully processed, save whatever here
40
+ # redirect_to_return_url :fb_return_to, "/home"
41
+ # end
42
+ #
43
+ # @param [Symbol] session_key the key in the session, defaults to
44
+ # :return_to.
45
+ # @param [String] default url when no stored value is found in
46
+ # session[session_key]. defaults to '/'.
47
+ def redirect_to_return_url(session_key = :return_to, default = '/')
48
+ redirect session.delete(:return_to) || default
29
49
  end
30
-
31
- def current_user
32
- @current_user ||= ::User[session[:user]] if session[:user]
50
+
51
+ # Returns the currently logged in user, identified through
52
+ # session[:user]. The default finder uses User[id], based on Ohm's
53
+ # finder method.
54
+ #
55
+ # @see http://ohm.keyvalue.org
56
+ #
57
+ # @example
58
+ #
59
+ # # ActiveRecord style finders
60
+ # current_user(lambda { |id| User.find(id) })
61
+ #
62
+ # # Also, if you change the settings to use a different user class,
63
+ # # then that will be respected
64
+ # set :login_user_class, :SuperUser
65
+ #
66
+ # # assuming session[:user] == 1
67
+ # current_user == SuperUser[1]
68
+ # # => true
69
+ #
70
+ # @param [Proc] finder (defaults to User[id]) allows you to pass in a
71
+ # different finder method.
72
+ # @return [User] or alternatively, an instance of settings.login_user_class
73
+ def current_user(finder = lambda { |id| __USER__[id] })
74
+ @current_user ||= finder.call(session[:user]) if session[:user]
33
75
  end
34
-
76
+
77
+ # @return [true] if the user is logged in
78
+ # @return [false] if the user is not logged in
35
79
  def logged_in?
36
80
  !! current_user
37
81
  end
38
-
82
+
83
+ # Used for simple atomic authorization rules on a per action / route
84
+ # basis.
85
+ #
86
+ # @example
87
+ #
88
+ # get '/posts/:id/edit' do |id|
89
+ # post = Post[id]
90
+ # ensure_current_user post.author # halts to a 404 if not satisfied.
91
+ #
92
+ # # the rest of this gets executed when
93
+ # # the author is indeed the current user.
94
+ # end
95
+ #
96
+ # @param [User] a user object.
39
97
  def ensure_current_user(user)
40
98
  halt 404 unless user == current_user
41
99
  end
42
-
100
+
101
+ # The method says it all. Mostly for keeping responsibility where it
102
+ # belongs, instead of letting the application code deal with the session
103
+ # keys themselves.
43
104
  def logout!
44
105
  session.delete(:user)
45
106
  end
107
+
108
+ # Internally used by the POST /login route handler.
109
+ #
110
+ # @param [Hash] opts The hash containing :username and :password.
111
+ # @option opts [#to_s] :username The username of a User.
112
+ # @option opts [String] :password The password of a User.
113
+ # @return [String] the `id` of the user if found.
114
+ # @return [nil] if no user matches the :username / :password combination.
115
+ def authenticate(opts)
116
+ if user = __USER__.authenticate(opts[:username], opts[:password])
117
+ session[:user] = user.id
118
+ end
119
+ end
46
120
 
47
- def should_return_to?(path)
48
- !(path =~ /(jpe?g|png|gif|css|js)$/)
121
+ # @private transforms settings.login_user_class to a constant,
122
+ # and used by current_user
123
+ def __USER__
124
+ Object.const_get(settings.login_user_class)
125
+ end
126
+
127
+ # @private internally used by Sinatra::Security::Helpers#require_login
128
+ def should_return_to?(path, ignored = settings.ignored_by_return_to)
129
+ !(path =~ ignored)
49
130
  end
50
131
  end
51
132
  end
@@ -1,17 +1,55 @@
1
1
  module Sinatra
2
2
  module Security
3
+ # The identification module is mixed into Sinatra::Security::User. The
4
+ # API loosely applies to Ohm, although it will work with ActiveRecord.
5
+ #
6
+ # @example
7
+ #
8
+ # class User < Ohm::Model
9
+ # include Sinatra::Security::User
10
+ # end
11
+ #
12
+ # User.respond_to?(:find_by_login)
13
+ # # => true
14
+ #
15
+ # User.respond_to?(:authenticate)
16
+ # # => true
17
+ #
18
+ # If you wish to override any of these, it's as simple as defining your
19
+ # own method in your User class.
3
20
  module Identification
4
- def find_by_login(login)
5
- find(:email => login).first
6
- end
7
-
8
- def authenticate(login, pass)
21
+ # Finds the User matching the given login / password combination.
22
+ # @see Sinatra::Security::Helpers#authenticate
23
+ #
24
+ # @param [#to_s] login the value of `:email` in your datastore.
25
+ # @param [String] password the raw password for the user in `:email`.
26
+ # @return [User] the user matching the credentials if found.
27
+ # @return [nil] if no matching user found.
28
+ def authenticate(login, password)
9
29
  if user = find_by_login(login)
10
- if Sinatra::Security::Password::Hashing.check(pass, user.crypted_password)
30
+ if Sinatra::Security::Password::Hashing.check(password, user.crypted_password)
11
31
  user
12
32
  end
13
33
  end
14
34
  end
35
+
36
+ # Used internally by User::authenticate to find the user given the
37
+ # `:email` value.
38
+ #
39
+ # @param [#to_s] login the value of `:email` in your datastore. You may
40
+ # also override the key used.
41
+ # @return [User] an instance of User if found.
42
+ # @return [nil] if no user found with the given login value.
43
+ #
44
+ # @see Sinatra::Security::LoginField::attr_name
45
+ def find_by_login(login)
46
+ find(__LOGIN_FIELD__ => login).first
47
+ end
48
+
49
+ protected
50
+ def __LOGIN_FIELD__
51
+ Sinatra::Security::LoginField.attr_name
52
+ end
15
53
  end
16
54
  end
17
55
  end
@@ -1,11 +1,44 @@
1
1
  module Sinatra
2
2
  module Security
3
+ # This module allows you to customize the name of the login field used
4
+ # in your datastore. By default :email is used.
5
+ #
6
+ # @example
7
+ #
8
+ # # somewhere in your code during bootstrapping
9
+ # require 'sinatra/security'
10
+ # Sinatra::Security::LoginField.attr_name :login
11
+ #
12
+ # # then in your actual user.rb or something...
13
+ # class User < Ohm::Model
14
+ # include Sinatra::Security::User
15
+ # # at this point the following are done:
16
+ # # attribute :login
17
+ # # index :login
18
+ # end
3
19
  module LoginField
20
+ # @example
21
+ #
22
+ # Sinatra::Security::LoginField.attr_name :username
23
+ #
24
+ # class User < Ohm::Model
25
+ # include Sinatra::Security::User
26
+ # # effectively executes the following:
27
+ # # attribute :username
28
+ # # index :username
29
+ # end
30
+ #
31
+ # @overload attr_name()
32
+ # Get the value of attr_name.
33
+ # @overload attr_name(attr_name)
34
+ # @param [#to_sym] attr_name the attr_name to be used e.g.
35
+ # :username, :login.
36
+ # @return [Symbol] the current attr_name.
4
37
  def self.attr_name(attr_name = nil)
5
- @attr_name = attr_name if attr_name
38
+ @attr_name = attr_name.to_sym if attr_name
6
39
  @attr_name
7
40
  end
8
- attr_name(:email)
41
+ attr_name :email
9
42
 
10
43
  def self.included(user)
11
44
  user.attribute LoginField.attr_name
@@ -13,4 +46,4 @@ module Sinatra
13
46
  end
14
47
  end
15
48
  end
16
- end
49
+ end
@@ -2,16 +2,36 @@ require 'digest/sha2'
2
2
 
3
3
  module Sinatra
4
4
  module Security
5
+ # This module handles everything related to password handling.
6
+ #
7
+ # @example
8
+ #
9
+ # class User < Ohm::Model
10
+ # include Sinatra::Security::Password
11
+ # end
12
+ #
13
+ # User.attributes == [:crypted_password]
14
+ # # => true
15
+ #
16
+ # User.new.respond_to?(:password)
17
+ # # => true
18
+ #
19
+ # User.new.respond_to?(:password_confirmation)
20
+ # # => true
21
+ #
22
+ # @see http://ohm.keyvalue.org for information about Ohm::Model.
5
23
  module Password
6
- def self.included(base)
7
- base.send :attribute, :crypted_password
8
- base.send :attr_accessor, :password, :password_confirmation
24
+ def self.included(model)
25
+ model.attribute :crypted_password
26
+
27
+ model.send :attr_accessor, :password, :password_confirmation
9
28
  end
10
29
 
11
30
  protected
31
+ # @private internally called by Ohm after validation when persisting.
12
32
  def write
13
33
  if !password.to_s.empty?
14
- write_local :crypted_password, Sinatra::Security::Password::Hashing.update(password)
34
+ write_local :crypted_password, Hashing.encrypt(password)
15
35
  end
16
36
 
17
37
  super
@@ -20,24 +40,61 @@ module Sinatra
20
40
  module Hashing
21
41
  extend self
22
42
 
23
- # Generates a new salt and rehashes the password
24
- def update(password)
25
- salt = self.salt
26
- hash = self.hash(password,salt)
27
- self.store(hash, salt)
43
+ # Given any string generates a string which includes both the
44
+ # 128 character (SHA512) hash and the salt.
45
+ #
46
+ # By default the salt is a 64 character pseudo-random string.
47
+ #
48
+ # @example
49
+ #
50
+ # include Sinatra::Security
51
+ #
52
+ # crypted = Password::Hashing.encrypt('123')
53
+ # crypted.length == 192
54
+ # # => true
55
+ #
56
+ # crypted = Password::Hashing.encrypt('123', '123456789012')
57
+ # crypted.length == 140 # i.e. 128 + 12 chars
58
+ # # => true
59
+ #
60
+ # crypted = Password::Hashing.encrypt('123')
61
+ # Password::Hashing.check('123', crypted)
62
+ # # => true
63
+ #
64
+ #
65
+ # @param [String] password any string with any length.
66
+ # @param [String] salt (defaults to Hashing#salt) any string.
67
+ # @return [String] a string holding the crypted password and the salt.
68
+ #
69
+ # @see Sinatra::Security::Password::Hashing#check
70
+ def encrypt(password, salt = self.generate_salt)
71
+ serialize(hash(password, salt), salt)
28
72
  end
29
73
 
30
- # Checks the password against the stored password
31
- def check(password, stored)
32
- hash = self.get_hash(stored)
33
- salt = self.get_salt(stored)
74
+ # Checks the password against the serialized password.
75
+ #
76
+ # @example
77
+ #
78
+ # include Sinatra::Security
79
+ # crypted = Password::Hashing.encrypt('123')
80
+ # Password::Hashing.check('123', crypted)
81
+ # # => true
82
+ #
83
+ # @param [String] password a string to check against crypted.
84
+ # @param [String] crypted the serialized string containing the
85
+ # hash and salt.
86
+ #
87
+ # @return [true] if the password matches the hash / salt combination.
88
+ # @return [false] if the password does not match the hash / salt.
89
+ def check(password, crypted)
90
+ hash, salt = unserialize(crypted)
34
91
 
35
92
  self.hash(password, salt) == hash
36
93
  end
37
94
 
38
95
  protected
39
96
  # Generates a psuedo-random 64 character string
40
- def salt
97
+ def generate_salt
41
98
  salt = ""
42
99
  64.times {
43
100
  salt << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }
@@ -50,18 +107,13 @@ module Sinatra
50
107
  end
51
108
 
52
109
  # Mixes the hash and salt together for storage
53
- def store(hash, salt)
110
+ def serialize(hash, salt)
54
111
  hash + salt
55
112
  end
56
-
57
- # Gets the hash from a stored password
58
- def get_hash(stored)
59
- stored[0..127]
60
- end
61
-
62
- # Gets the salt from a stored password
63
- def get_salt(stored)
64
- stored[128..192]
113
+
114
+ # Returns the original hash and salt generated from serialize.
115
+ def unserialize(serialized)
116
+ return serialized[0..127], serialized[128..-1]
65
117
  end
66
118
  end
67
119
  end
@@ -1,5 +1,30 @@
1
1
  module Sinatra
2
2
  module Security
3
+ # Mixes in to any included class all of the following:
4
+ #
5
+ # * Sinatra::Security::LoginField
6
+ # * Sinatra::Security::Password
7
+ # * Sinatra::Security::Validations
8
+ #
9
+ # It also extends the class with Sinatra::Security::Identification.
10
+ #
11
+ # @example
12
+ #
13
+ # class User < Ohm::Model
14
+ # include Sinatra::Security::User
15
+ # end
16
+ #
17
+ # user = User.new
18
+ # user.valid?
19
+ # user.errors == [[:email, :not_present], [:password, :not_present]]
20
+ # # => true
21
+ #
22
+ # user = User.create(:email => "test@example.com", :password => "pass",
23
+ # :password_confirmation => "pass")
24
+ #
25
+ # User.authenticate("test@example.com", "pass") == user
26
+ # # => true
27
+ #
3
28
  module User
4
29
  def self.included(user)
5
30
  user.send :include, LoginField
@@ -10,4 +35,4 @@ module Sinatra
10
35
  end
11
36
  end
12
37
  end
13
- end
38
+ end
@@ -4,11 +4,14 @@ module Sinatra
4
4
  EMAIL_FORMAT = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
5
5
 
6
6
  def validate
7
- # TODO : email requirement should only be done if
8
- # LoginField.attr_name == :email maybe
9
- # then just let users expilicity declare there own
10
- # validation rules
11
- assert_login_using_email :email
7
+ login_field = Sinatra::Security::LoginField.attr_name
8
+
9
+ if login_field == :email
10
+ assert_login_using_email :email
11
+ else
12
+ assert_present(login_field) and assert_unique(login_field)
13
+ end
14
+
12
15
  assert_password :password
13
16
 
14
17
  super
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{sinatra-security}
8
- s.version = "0.1.6"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Cyril David"]
12
- s.date = %q{2010-05-18}
12
+ s.date = %q{2010-05-26}
13
13
  s.description = %q{For use with Sinatra + Monk + OHM}
14
14
  s.email = %q{cyx.ucron@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
35
35
  "lib/sinatra/security/validations.rb",
36
36
  "sinatra-security.gemspec",
37
37
  "test/helper.rb",
38
+ "test/test_login_field_flexibility.rb",
38
39
  "test/test_password.rb",
39
40
  "test/test_sinatra-security.rb",
40
41
  "test/test_sinatra_security_helpers.rb",
@@ -44,10 +45,11 @@ Gem::Specification.new do |s|
44
45
  s.homepage = %q{http://github.com/sinefunc/sinatra-security}
45
46
  s.rdoc_options = ["--charset=UTF-8"]
46
47
  s.require_paths = ["lib"]
47
- s.rubygems_version = %q{1.3.5}
48
+ s.rubygems_version = %q{1.3.6}
48
49
  s.summary = %q{Sinatra authentication extension}
49
50
  s.test_files = [
50
51
  "test/helper.rb",
52
+ "test/test_login_field_flexibility.rb",
51
53
  "test/test_password.rb",
52
54
  "test/test_sinatra-security.rb",
53
55
  "test/test_sinatra_security_helpers.rb",
@@ -0,0 +1,37 @@
1
+ require "helper"
2
+
3
+ class LoginFieldFlexbilityTest < Test::Unit::TestCase
4
+ def setup
5
+ Sinatra::Security::LoginField.attr_name :login
6
+ end
7
+
8
+ def teardown
9
+ Sinatra::Security::LoginField.attr_name :email
10
+ end
11
+
12
+ class DiffUser
13
+ def self.attribute(att)
14
+ end
15
+
16
+ def self.index(att)
17
+ end
18
+
19
+ include Sinatra::Security::User
20
+ end
21
+
22
+ test "find_by_login uses :login" do
23
+ @set = stub("Set", :first => :user)
24
+ DiffUser.expects(:find).with(:login => 'foobar').returns(@set)
25
+
26
+ assert_equal :user, DiffUser.find_by_login('foobar')
27
+ end
28
+
29
+ test "authenticate method uses :login" do
30
+ @user = stub("User", :id => 1001, :crypted_password => '_crypted_')
31
+ @set = stub("Set", :first => @user)
32
+ DiffUser.expects(:find).with(:login => 'foobar').returns(@set)
33
+
34
+
35
+ DiffUser.authenticate('foobar', 'pass')
36
+ end
37
+ end
@@ -21,8 +21,13 @@ class TestPassword < Test::Unit::TestCase
21
21
  include Sinatra::Security
22
22
 
23
23
  describe "#update" do
24
- should "return a 192 character string" do
25
- assert_equal 192, Password::Hashing.update('123456').length
24
+ should "return a 192 character string by default" do
25
+ assert_equal 192, Password::Hashing.encrypt('123456').length
26
+ end
27
+
28
+ should "return a different length if you pass a custom salt" do
29
+ assert_equal 140,
30
+ Password::Hashing.encrypt('123456', '123456789012').length
26
31
  end
27
32
  end
28
33
 
@@ -30,7 +35,14 @@ class TestPassword < Test::Unit::TestCase
30
35
  should "be able to match the original password given" do
31
36
  @password = 'password100'
32
37
 
33
- assert Password::Hashing.check(@password, Password::Hashing.update(@password))
38
+ assert Password::Hashing.check(@password, Password::Hashing.encrypt(@password))
39
+ end
40
+
41
+ should "be able to match original pass given custom salt" do
42
+ password = 'password100'
43
+ crypted = Password::Hashing.encrypt(password, 'customsalt')
44
+
45
+ assert Password::Hashing.check(password, crypted)
34
46
  end
35
47
  end
36
48
 
@@ -5,6 +5,8 @@ class BasicApp < Sinatra::Base
5
5
 
6
6
  register Sinatra::Security
7
7
 
8
+ require_login '/mass'
9
+
8
10
  get '/login' do
9
11
  "<h1>Login Page</h1>"
10
12
  end
@@ -16,6 +18,14 @@ class BasicApp < Sinatra::Base
16
18
  get '/private' do
17
19
  require_login
18
20
  end
21
+
22
+ get '/mass/private1' do
23
+ "Private 1"
24
+ end
25
+
26
+ get '/mass/private2' do
27
+ "Private 2"
28
+ end
19
29
 
20
30
  get '/css/main.css' do
21
31
  require_login
@@ -136,4 +146,23 @@ class TestSinatraSecurity < Test::Unit::TestCase
136
146
  assert_equal '/login', last_response.headers['Location']
137
147
  end
138
148
  end
149
+
150
+ describe "going to /mass/private1" do
151
+ should "redirect to /login" do
152
+ get '/mass/private1'
153
+
154
+ assert_equal 302, last_response.status
155
+ assert_equal '/login', last_response.headers['Location']
156
+ end
157
+ end
158
+
159
+ describe "going to /mass/private2" do
160
+ should "redirect to /login" do
161
+ get '/mass/private2'
162
+
163
+ assert_equal 302, last_response.status
164
+ assert_equal '/login', last_response.headers['Location']
165
+ end
166
+ end
167
+
139
168
  end
@@ -27,6 +27,9 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
27
27
 
28
28
  describe "when session[:user] is set to 1" do
29
29
  setup do
30
+ @settings = stub("Settings", :login_user_class => :User)
31
+
32
+ @context.stubs(:settings).returns(@settings)
30
33
  @context.session[:user] = 1
31
34
  end
32
35
 
@@ -83,15 +86,27 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
83
86
 
84
87
  describe "#require_login" do
85
88
  context "when logged_in?" do
86
- should "return true" do
89
+ should "not redirect" do
90
+ @settings = stub("Settings", :ignored_by_return_to => /foobared$/,
91
+ :login_url => '/new-login-url')
92
+ @context.stubs(:settings).returns(@settings)
93
+
94
+
95
+ @context.expects(:redirect).never
87
96
  @context.expects(:logged_in?).returns(true)
88
- assert @context.require_login
97
+
98
+ @context.require_login
89
99
  end
90
100
  end
91
101
 
92
102
  context "when not logged_in?" do
93
103
  setup do
94
104
  @context.stubs(:logged_in?).returns(false)
105
+ @settings = stub("Settings", :ignored_by_return_to => /foobared$/,
106
+ :login_url => '/new-login-url')
107
+
108
+ @context.stubs(:settings).returns(@settings)
109
+
95
110
  @context.request = stub("Request", :fullpath => "/some/fullpath/here")
96
111
  end
97
112
 
@@ -102,7 +117,7 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
102
117
  end
103
118
 
104
119
  should "redirect to /login" do
105
- @context.expects(:redirect).with('/login')
120
+ @context.expects(:redirect).with('/new-login-url')
106
121
 
107
122
  @context.require_login
108
123
  end
@@ -110,6 +125,15 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
110
125
  should "return false" do
111
126
  assert ! @context.require_login
112
127
  end
128
+
129
+ context "when the request matches ignored" do
130
+ should "not store the fullpath in return_to" do
131
+ @context.request = stub("Request", :fullpath => "/some/fullpath/foobared")
132
+ @context.require_login
133
+
134
+ assert_nil @context.session[:return_to]
135
+ end
136
+ end
113
137
  end
114
138
  end
115
139
 
@@ -46,6 +46,64 @@ class UserDefaultValidation < TestFixtures::User
46
46
  end
47
47
 
48
48
  class TestValidations < Test::Unit::TestCase
49
+ context "with a different login field" do
50
+ class User < TestFixtures::User
51
+ include Sinatra::Security::Validations
52
+
53
+ attr_accessor :login
54
+
55
+ def errors
56
+ @errors ||= []
57
+ end
58
+ end
59
+
60
+ setup do
61
+ Sinatra::Security::LoginField.attr_name :login
62
+ end
63
+
64
+ teardown do
65
+ Sinatra::Security::LoginField.attr_name :email
66
+ end
67
+
68
+ context "when a new record" do
69
+ should "assert login present" do
70
+ user = User.new
71
+ user.expects(:new?).returns(true)
72
+ user.validate
73
+
74
+ assert_equal [[:login, :not_present], [:password, :not_present]],
75
+ user.errors
76
+ end
77
+
78
+ should "assert login unique" do
79
+ user = User.new
80
+ user.login = 'login'
81
+ user.expects(:new?).returns(true)
82
+ user.expects(:assert_unique).with(:login)
83
+ user.validate
84
+ end
85
+ end
86
+
87
+ context "when an existing record" do
88
+ should "still assert login present" do
89
+ user = User.new
90
+ user.expects(:new?).returns(false)
91
+ user.validate
92
+
93
+ assert_equal [[:login, :not_present]], user.errors
94
+ end
95
+
96
+ should "assert login unique" do
97
+ user = User.new
98
+ user.login = 'login'
99
+ user.expects(:new?).returns(false)
100
+ user.expects(:assert_unique).with(:login)
101
+ user.validate
102
+ end
103
+ end
104
+
105
+ end
106
+
49
107
  context "without an email" do
50
108
  should "require the email to be present" do
51
109
  user = UserWithEmailValidation.new
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-security
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Cyril David
@@ -9,7 +14,7 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2010-05-18 00:00:00 +08:00
17
+ date: 2010-05-26 00:00:00 +08:00
13
18
  default_executable:
14
19
  dependencies: []
15
20
 
@@ -41,6 +46,7 @@ files:
41
46
  - lib/sinatra/security/validations.rb
42
47
  - sinatra-security.gemspec
43
48
  - test/helper.rb
49
+ - test/test_login_field_flexibility.rb
44
50
  - test/test_password.rb
45
51
  - test/test_sinatra-security.rb
46
52
  - test/test_sinatra_security_helpers.rb
@@ -59,23 +65,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
59
65
  requirements:
60
66
  - - ">="
61
67
  - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
62
70
  version: "0"
63
- version:
64
71
  required_rubygems_version: !ruby/object:Gem::Requirement
65
72
  requirements:
66
73
  - - ">="
67
74
  - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
68
77
  version: "0"
69
- version:
70
78
  requirements: []
71
79
 
72
80
  rubyforge_project:
73
- rubygems_version: 1.3.5
81
+ rubygems_version: 1.3.6
74
82
  signing_key:
75
83
  specification_version: 3
76
84
  summary: Sinatra authentication extension
77
85
  test_files:
78
86
  - test/helper.rb
87
+ - test/test_login_field_flexibility.rb
79
88
  - test/test_password.rb
80
89
  - test/test_sinatra-security.rb
81
90
  - test/test_sinatra_security_helpers.rb