winton-authentication 1.0.0

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.
data/CHANGELOG ADDED
@@ -0,0 +1,74 @@
1
+ 2008-06-23 - Sean Huber (shuber@huberry.com)
2
+ * Renamed authentication_message/redirect_path to unauthenticated_message/redirect_path
3
+ * logged_in? now accepts a block that will yield when true
4
+
5
+ 2008-06-09 - Sean Huber (shuber@huberry.com)
6
+ * unauthenticated method stores request_uri in session[:return_to]
7
+
8
+ 2008-06-09 - Sean Huber (shuber@huberry.com)
9
+ * Removed accidently left over hard coded logic from password_required?
10
+
11
+ 2008-06-09 - Sean Huber (shuber@huberry.com)
12
+ * Reloading a model sets password_changed? to false
13
+
14
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
15
+ * Changed email addresses in CHANGELOG
16
+
17
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
18
+ * Added find_current_user functional tests
19
+
20
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
21
+ * Renamed test files
22
+ * find_current_user always returns current_user
23
+
24
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
25
+ * Renamed login_required to authentication_required
26
+
27
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
28
+ * Renamed @searched_for_current_user to @queried_for_current_user
29
+
30
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
31
+ * Added extra uses_authentication field options
32
+ * Updated README
33
+
34
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
35
+ * current_user returns nil unless logged_in?
36
+ * Updated README
37
+
38
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
39
+ * Updated README
40
+
41
+ 2008-06-05 - Sean Huber (shuber@huberry.com)
42
+ * Updated README
43
+
44
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
45
+ * Updated README
46
+
47
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
48
+ * Updated README
49
+
50
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
51
+ * Controller no longer has to call uses_authentication - logic included automatically
52
+ * Updated README
53
+
54
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
55
+ * Updated README
56
+
57
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
58
+ * Updated README
59
+
60
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
61
+ * Updated README
62
+
63
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
64
+ * Added functional tests
65
+
66
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
67
+ * Fixed functional test exception
68
+
69
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
70
+ * Controller and model must call uses_authentication
71
+
72
+ 2008-06-04 - Sean Huber (shuber@huberry.com)
73
+ * Initial import from access_control_list repository
74
+ * Login field is configurable
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Sean Huber (shuber@huberry.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,82 @@
1
+ Huberry::Authentication
2
+ =======================
3
+
4
+ A rails plugin that handles authentication
5
+
6
+
7
+ Installation
8
+ ------------
9
+
10
+ script/plugin install git://github.com/shuber/authentication.git
11
+
12
+
13
+ Example
14
+ -------
15
+
16
+ class User < ActiveRecord::Base
17
+ # Accepts an optional hash of options
18
+ # :login_field - The field to use for logins (e.g. username or email) (defaults to :email)
19
+ # :password_field - (defaults to :password)
20
+ # :hashed_password_field - (defaults to :hashed_password)
21
+ # :salt_field - (defaults to :salt)
22
+ uses_authentication :login_field => :username
23
+ end
24
+
25
+ class ApplicationController < ActionController::Base
26
+ # Set optional authentication options here
27
+ # self.authentication_model = The model that uses authentication (defaults to 'User')
28
+ # self.unauthenticated_message = The error flash message to set when unauthenticated (defaults to 'Login to continue')
29
+ # self.unauthenticated_redirect_path = The path to redirect to when unauthenticated (can be a symbol of a method) (defaults to '/')
30
+ end
31
+
32
+ class UsersController < ApplicationController
33
+ before_filter :login_required, :only => [:index]
34
+
35
+ def index
36
+ render :text => 'test'
37
+ end
38
+ end
39
+
40
+
41
+ Controller Methods
42
+ ------------------
43
+
44
+ # Returns the current user or nil if a user is not logged in
45
+ current_user
46
+
47
+ # Checks if the current user is authenticated (optionaly accepts a block that is yielded when true)
48
+ logged_in?
49
+
50
+ # Login a user
51
+ login(user)
52
+
53
+ # A before filter to require authentication - redirects to the controller class's authentication_redirect_path if unauthenticated
54
+ login_required
55
+
56
+ # Logout the current user
57
+ logout
58
+
59
+
60
+ Model Methods
61
+ -------------
62
+
63
+ # Class method that authenticates a user based on a login and password - returns a user instance or false
64
+ User.authenticate(login, password)
65
+
66
+ # Checks if the password passed to it matches the current user instance's password
67
+ authenticated?(password)
68
+
69
+ # Checks if the current user instance's password has just been changed
70
+ password_changed?
71
+
72
+ # Resets the password - will generate a new random password if one is not specified
73
+ reset_password(new_password = nil)
74
+
75
+ # Resets the password and saves - will generate a new random password if one is not specified
76
+ reset_password!(new_password = nil)
77
+
78
+
79
+ Contact
80
+ -------
81
+
82
+ Problems, comments, and suggestions all welcome: [shuber@huberry.com](mailto:shuber@huberry.com)
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run the authentication tests'
6
+ task :default => :test
7
+
8
+ desc 'Test the authentication plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the authentication plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Authentication'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'huberry/authentication/controller_methods'
2
+ require 'huberry/authentication/model_methods'
3
+
4
+ ActionController::Base.extend Huberry::Authentication::ControllerMethods
5
+ ActiveRecord::Base.extend Huberry::Authentication::ModelMethods
6
+
7
+ $:.unshift File.dirname(__FILE__) + '/lib'
@@ -0,0 +1,61 @@
1
+ module Huberry
2
+ module Authentication
3
+ module ControllerMethods
4
+ def self.extended(base)
5
+ base.class_eval do
6
+ include InstanceMethods
7
+
8
+ cattr_accessor :authentication_model, :unauthenticated_message, :unauthenticated_redirect_path
9
+ self.authentication_model = 'User'
10
+ self.unauthenticated_message = 'Login to continue'
11
+ self.unauthenticated_redirect_path = '/'
12
+
13
+ attr_accessor :current_user
14
+ helper_method :current_user, :logged_in?
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ protected
20
+
21
+ def authentication_required
22
+ unauthenticated unless logged_in?
23
+ end
24
+
25
+ def find_current_user(force_query = false)
26
+ if @queried_for_current_user.nil? || force_query
27
+ @queried_for_current_user = true
28
+ self.current_user = self.class.authentication_model.to_s.constantize.find(session[:user_id]) rescue nil
29
+ end
30
+ self.current_user
31
+ end
32
+
33
+ def logged_in?
34
+ find_current_user if self.current_user.nil?
35
+ if self.current_user.nil?
36
+ block_given? ? nil : false
37
+ else
38
+ block_given? ? yield : true
39
+ end
40
+ end
41
+
42
+ def login(user)
43
+ self.current_user = user
44
+ session[:user_id] = user.id
45
+ end
46
+
47
+ def logout
48
+ self.current_user = nil
49
+ session[:user_id] = nil
50
+ end
51
+
52
+ def unauthenticated
53
+ session[:return_to] = request.request_uri
54
+ flash[:error] = self.class.unauthenticated_message.to_s
55
+ redirect_to respond_to?(self.class.unauthenticated_redirect_path) ? send(self.class.unauthenticated_redirect_path) : self.class.unauthenticated_redirect_path.to_s
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,84 @@
1
+ require 'digest/sha2'
2
+
3
+ module Huberry
4
+ module Authentication
5
+ module ModelMethods
6
+ def uses_authentication(options = {})
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ cattr_accessor :hashed_password_field, :login_field, :password_field, :salt_field
11
+ self.hashed_password_field = options[:hashed_password_field] || :hashed_password
12
+ self.login_field = options[:login_field] || :email
13
+ self.password_field = options[:password_field] || :password
14
+ self.salt_field = options[:salt_field] || :salt
15
+
16
+ attr_accessor self.password_field, "#{self.password_field}_confirmation".to_sym
17
+
18
+ validates_presence_of self.login_field
19
+ validates_presence_of self.password_field, :if => :password_required?
20
+ validates_confirmation_of self.password_field, :if => :password_required?
21
+
22
+ before_save :hash_password
23
+
24
+ alias_method_chain :reload, :authentication
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ def authenticate(login, password)
30
+ user = send("find_by_#{self.login_field}", login)
31
+ user && user.authenticated?(password) ? user : false
32
+ end
33
+
34
+ def digest(str)
35
+ Digest::SHA256.hexdigest(str.to_s)
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+ def authenticated?(password)
41
+ send(self.class.hashed_password_field) == self.class.digest(password.to_s + send(self.class.salt_field).to_s)
42
+ end
43
+
44
+ def password_changed?
45
+ !!@password_changed
46
+ end
47
+
48
+ def reload_with_authentication(*args)
49
+ reload_without_authentication(*args)
50
+ @password_changed = false
51
+ end
52
+
53
+ def reset_password(new_password = nil)
54
+ new_password = generate_salt[0..7] if new_password.blank?
55
+ send("#{self.class.salt_field}=", nil)
56
+ send("#{self.class.password_field}=", new_password)
57
+ send("#{self.class.password_field}_confirmation=", new_password)
58
+ @password_changed = true
59
+ end
60
+
61
+ def reset_password!(new_password = nil)
62
+ reset_password(new_password)
63
+ save
64
+ end
65
+
66
+ protected
67
+
68
+ def generate_salt
69
+ self.class.digest("--#{Time.now}--#{rand}--")
70
+ end
71
+
72
+ def hash_password
73
+ if password_changed? || password_required?
74
+ send("#{self.class.salt_field}=", generate_salt) if send("#{self.class.salt_field}").blank?
75
+ send("#{self.class.hashed_password_field}=", self.class.digest(send(self.class.password_field).to_s + send(self.class.salt_field).to_s))
76
+ end
77
+ end
78
+
79
+ def password_required?
80
+ send(self.class.hashed_password_field).blank? || !send(self.class.password_field).blank?
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,114 @@
1
+ require File.dirname(__FILE__) + '/init'
2
+
3
+ class TestController < ActionController::Base
4
+ before_filter :authentication_required, :only => :login_required
5
+
6
+ def login_required
7
+ render :text => 'test'
8
+ end
9
+
10
+ def login_not_required
11
+ render :text => 'test'
12
+ end
13
+
14
+ def rescue_action(e)
15
+ raise e
16
+ end
17
+ end
18
+
19
+ ActionController::Routing::Routes.append do |map|
20
+ map.connect 'login_required', :controller => 'test', :action => 'login_required'
21
+ map.connect 'login_not_required', :controller => 'test', :action => 'login_not_required'
22
+ end
23
+
24
+ class ControllerTest < Test::Unit::TestCase
25
+
26
+ def setup
27
+ create_users_table
28
+ @user = create_user
29
+
30
+ @controller = TestController.new
31
+ @request = ActionController::TestRequest.new
32
+ @response = ActionController::TestResponse.new
33
+
34
+ @controller.instance_variable_set('@_session', @request.session)
35
+ end
36
+
37
+ def teardown
38
+ drop_all_tables
39
+ end
40
+
41
+ def test_logged_in?
42
+ assert !@controller.send(:logged_in?)
43
+
44
+ @controller.send :login, @user
45
+ assert @controller.send(:logged_in?)
46
+
47
+ @controller.send :logout
48
+ assert !@controller.send(:logged_in?)
49
+ end
50
+
51
+ def test_logged_in_yields_block_on_true
52
+ assert_equal nil, @controller.send(:logged_in?) { 'some_value' }
53
+
54
+ @controller.send :login, @user
55
+ assert_equal 'some_value', @controller.send(:logged_in?) { 'some_value' }
56
+ end
57
+
58
+ def test_current_user
59
+ assert_nil @controller.send(:current_user)
60
+
61
+ @controller.send :login, @user
62
+ assert_equal @user, @controller.send(:current_user)
63
+
64
+ @controller.send :logout
65
+ assert_nil @controller.send(:current_user)
66
+ end
67
+
68
+ def test_should_require_login
69
+ get :login_required
70
+ assert_response :redirect
71
+ assert flash.has_key?(:error)
72
+ assert_equal @controller.unauthenticated_message, flash[:error]
73
+ assert_redirected_to '/'
74
+ end
75
+
76
+ def test_should_not_require_login
77
+ get :login_not_required
78
+ assert_response :success
79
+ end
80
+
81
+ def test_should_login_and_get_authentication_required
82
+ @controller.send :login, @user
83
+ get :login_required
84
+ assert_response :success
85
+ end
86
+
87
+ def test_find_current_user_with_valid_user
88
+ @request.session[:user_id] = @user.id
89
+ assert_equal @user, @controller.send(:find_current_user)
90
+ end
91
+
92
+ def test_find_current_user_with_nil_user
93
+ assert_nil @controller.send(:find_current_user)
94
+ end
95
+
96
+ def test_find_current_user_with_invalid_user
97
+ @request.session[:user_id] = 2
98
+ assert_nil @controller.send(:find_current_user)
99
+ end
100
+
101
+ def test_find_current_user_force_query
102
+ assert_nil @controller.send(:find_current_user)
103
+
104
+ @request.session[:user_id] = @user.id
105
+ assert_nil @controller.send(:find_current_user)
106
+ assert_equal @user, @controller.send(:find_current_user, true)
107
+ end
108
+
109
+ def test_unauthenticated_sets_return_to_session_variable
110
+ get :login_required
111
+ assert_equal @controller.session[:return_to], '/login_required'
112
+ end
113
+
114
+ end
@@ -0,0 +1,99 @@
1
+ require File.dirname(__FILE__) + '/init'
2
+
3
+ class ModelTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ create_users_table
7
+ end
8
+
9
+ def teardown
10
+ drop_all_tables
11
+ end
12
+
13
+ def test_should_create_user
14
+ assert_difference 'User.count' do
15
+ create_user
16
+ end
17
+ end
18
+
19
+ def test_should_not_create_without_email
20
+ assert_no_difference 'User.count' do
21
+ @user = create_user :email => ''
22
+ end
23
+ assert @user.errors.on(:email)
24
+ end
25
+
26
+ def test_should_not_create_without_password
27
+ assert_no_difference 'User.count' do
28
+ @user = create_user :password => ''
29
+ end
30
+ assert @user.errors.on(:password)
31
+ end
32
+
33
+ def test_should_not_create_without_password_confirmation
34
+ assert_no_difference 'User.count' do
35
+ @user = create_user :password_confirmation => ''
36
+ end
37
+ assert @user.errors.on(:password)
38
+ end
39
+
40
+ def test_should_generate_salt_on_create
41
+ @user = create_user
42
+ assert !@user.salt.blank?
43
+ end
44
+
45
+ def test_should_hash_password_on_create
46
+ @user = create_user
47
+ assert !@user.hashed_password.blank?
48
+ end
49
+
50
+ def test_should_destroy
51
+ @user = create_user
52
+ assert_difference 'User.count', -1 do
53
+ @user.destroy
54
+ end
55
+ end
56
+
57
+ def test_should_authenticate
58
+ @user = create_user
59
+ assert_equal @user, User.authenticate(@user.email, @user.password)
60
+ end
61
+
62
+ def test_should_not_authenticate_with_invalid_email
63
+ @user = create_user
64
+ assert !User.authenticate('invalid', @user.password)
65
+ end
66
+
67
+ def test_should_not_authenticate_with_invalid_password
68
+ @user = create_user
69
+ assert !User.authenticate(@user.email, 'invalid')
70
+ end
71
+
72
+ def test_should_reset_password
73
+ @user = create_user
74
+ assert !@user.password_changed?
75
+ old_password = @user.password
76
+
77
+ @user.reset_password('new_password')
78
+ assert_not_equal 'new_password', old_password
79
+ assert_equal 'new_password', @user.password
80
+ assert @user.password_changed?
81
+ end
82
+
83
+ def test_should_reset_password!
84
+ @user = create_user
85
+ assert @user.reset_password!('new_password')
86
+ end
87
+
88
+ def test_reload_should_set_password_changed_to_false
89
+ @user = create_user
90
+ assert !@user.password_changed?
91
+
92
+ @user.reset_password!('new_password')
93
+ assert @user.password_changed?
94
+
95
+ @user.reload
96
+ assert !@user.password_changed?
97
+ end
98
+
99
+ end
@@ -0,0 +1,25 @@
1
+ module TableTestHelper
2
+
3
+ def create_users_table
4
+ silence_stream(STDOUT) do
5
+ ActiveRecord::Schema.define(:version => 1) do
6
+ create_table :users do |t|
7
+ t.string :email
8
+ t.string :hashed_password
9
+ t.string :salt
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ def drop_all_tables
16
+ ActiveRecord::Base.connection.tables.each do |table|
17
+ drop_table(table)
18
+ end
19
+ end
20
+
21
+ def drop_table(table)
22
+ ActiveRecord::Base.connection.drop_table(table)
23
+ end
24
+
25
+ end
@@ -0,0 +1,11 @@
1
+ module UserTestHelper
2
+
3
+ def create_user(options = {})
4
+ User.create(valid_user_hash(options))
5
+ end
6
+
7
+ def valid_user_hash(options = {})
8
+ { :email => 'test@test.com', :password => 'test', :password_confirmation => 'test' }.merge(options)
9
+ end
10
+
11
+ end
data/test/init.rb ADDED
@@ -0,0 +1,45 @@
1
+ $:.reject! { |path| path.include? 'TextMate' }
2
+ require 'test/unit'
3
+
4
+ # Require and include test helpers
5
+ #
6
+ Dir[File.join(File.dirname(__FILE__), 'helpers', '*_test_helper.rb')].each do |helper|
7
+ require helper
8
+ /(.*?)_test_helper\.rb/.match File.basename(helper)
9
+ class_name = $1.split('_').collect{ |name| name.downcase.capitalize }.join('') + 'TestHelper'
10
+ Test::Unit::TestCase.send :include, Object.const_get(class_name) if Object.const_defined?(class_name)
11
+ end
12
+
13
+ # Load ActiveRecord
14
+ #
15
+ require 'rubygems'
16
+ gem 'activerecord'
17
+ require 'active_record'
18
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :dbfile => ':memory:'
19
+
20
+ # Load ActionPack
21
+ #
22
+ gem 'actionpack'
23
+ require 'action_pack'
24
+ require 'action_controller'
25
+ require 'action_controller/routing'
26
+ require 'action_controller/assertions'
27
+ require 'action_controller/test_process'
28
+
29
+ # Routing
30
+ #
31
+ class ActionController::Routing::RouteSet
32
+ def append
33
+ yield Mapper.new(self)
34
+ install_helpers
35
+ end
36
+ end
37
+
38
+ # Require the main init.rb for the plugin
39
+ #
40
+ require File.join(File.dirname(File.dirname(__FILE__)), 'init')
41
+
42
+ # Basic user class
43
+ class User < ActiveRecord::Base
44
+ uses_authentication
45
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winton-authentication
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Huber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A rails plugin that handles authentication
17
+ email: shuber@huberry.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - CHANGELOG
26
+ - init.rb
27
+ - lib/huberry/authentication/controller_methods.rb
28
+ - lib/huberry/authentication/model_methods.rb
29
+ - MIT-LICENSE
30
+ - Rakefile
31
+ - README.markdown
32
+ has_rdoc: false
33
+ homepage: http://github.com/shuber/authentication
34
+ post_install_message:
35
+ rdoc_options: []
36
+
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project:
54
+ rubygems_version: 1.2.0
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: A rails plugin that handles authentication
58
+ test_files:
59
+ - test/_controller_test.rb
60
+ - test/_model_test.rb
61
+ - test/helpers/table_test_helper.rb
62
+ - test/helpers/user_test_helper.rb
63
+ - test/init.rb