resource_controller 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (211) hide show
  1. data/LICENSE +22 -0
  2. data/README +282 -0
  3. data/README.rdoc +282 -0
  4. data/Rakefile +35 -0
  5. data/generators/scaffold_resource/USAGE +29 -0
  6. data/generators/scaffold_resource/scaffold_resource_generator.rb +179 -0
  7. data/generators/scaffold_resource/templates/controller.rb +2 -0
  8. data/generators/scaffold_resource/templates/fixtures.yml +10 -0
  9. data/generators/scaffold_resource/templates/functional_test.rb +57 -0
  10. data/generators/scaffold_resource/templates/helper.rb +2 -0
  11. data/generators/scaffold_resource/templates/migration.rb +15 -0
  12. data/generators/scaffold_resource/templates/model.rb +2 -0
  13. data/generators/scaffold_resource/templates/old_migration.rb +13 -0
  14. data/generators/scaffold_resource/templates/rspec/functional_spec.rb +255 -0
  15. data/generators/scaffold_resource/templates/rspec/helper_spec.rb +11 -0
  16. data/generators/scaffold_resource/templates/rspec/routing_spec.rb +61 -0
  17. data/generators/scaffold_resource/templates/rspec/unit_spec.rb +11 -0
  18. data/generators/scaffold_resource/templates/rspec/views/edit_spec.rb +28 -0
  19. data/generators/scaffold_resource/templates/rspec/views/index_spec.rb +26 -0
  20. data/generators/scaffold_resource/templates/rspec/views/new_spec.rb +30 -0
  21. data/generators/scaffold_resource/templates/rspec/views/show_spec.rb +25 -0
  22. data/generators/scaffold_resource/templates/shoulda_functional_test.rb +19 -0
  23. data/generators/scaffold_resource/templates/unit_test.rb +7 -0
  24. data/generators/scaffold_resource/templates/view__form.erb +6 -0
  25. data/generators/scaffold_resource/templates/view__form.haml +5 -0
  26. data/generators/scaffold_resource/templates/view_edit.erb +16 -0
  27. data/generators/scaffold_resource/templates/view_edit.haml +11 -0
  28. data/generators/scaffold_resource/templates/view_index.erb +22 -0
  29. data/generators/scaffold_resource/templates/view_index.haml +19 -0
  30. data/generators/scaffold_resource/templates/view_new.erb +12 -0
  31. data/generators/scaffold_resource/templates/view_new.haml +9 -0
  32. data/generators/scaffold_resource/templates/view_show.erb +9 -0
  33. data/generators/scaffold_resource/templates/view_show.haml +9 -0
  34. data/init.rb +1 -0
  35. data/lib/resource_controller.rb +15 -0
  36. data/lib/resource_controller/accessors.rb +77 -0
  37. data/lib/resource_controller/action_options.rb +40 -0
  38. data/lib/resource_controller/actions.rb +75 -0
  39. data/lib/resource_controller/base.rb +15 -0
  40. data/lib/resource_controller/class_methods.rb +22 -0
  41. data/lib/resource_controller/controller.rb +63 -0
  42. data/lib/resource_controller/failable_action_options.rb +25 -0
  43. data/lib/resource_controller/helpers.rb +28 -0
  44. data/lib/resource_controller/helpers/current_objects.rb +69 -0
  45. data/lib/resource_controller/helpers/internal.rb +76 -0
  46. data/lib/resource_controller/helpers/nested.rb +45 -0
  47. data/lib/resource_controller/helpers/urls.rb +124 -0
  48. data/lib/resource_controller/response_collector.rb +27 -0
  49. data/lib/resource_controller/version.rb +9 -0
  50. data/lib/tasks/gem.rake +67 -0
  51. data/lib/urligence.rb +50 -0
  52. data/rails/init.rb +6 -0
  53. data/test/Rakefile +10 -0
  54. data/test/app/controllers/application.rb +7 -0
  55. data/test/app/controllers/cms/options_controller.rb +3 -0
  56. data/test/app/controllers/cms/products_controller.rb +3 -0
  57. data/test/app/controllers/comments_controller.rb +3 -0
  58. data/test/app/controllers/people_controller.rb +9 -0
  59. data/test/app/controllers/photos_controller.rb +11 -0
  60. data/test/app/controllers/posts_controller.rb +10 -0
  61. data/test/app/controllers/projects_controller.rb +3 -0
  62. data/test/app/controllers/somethings_controller.rb +3 -0
  63. data/test/app/controllers/tags_controller.rb +13 -0
  64. data/test/app/controllers/users_controller.rb +12 -0
  65. data/test/app/helpers/application_helper.rb +3 -0
  66. data/test/app/helpers/cms/products_helper.rb +2 -0
  67. data/test/app/helpers/comments_helper.rb +2 -0
  68. data/test/app/helpers/options_helper.rb +2 -0
  69. data/test/app/helpers/people_helper.rb +2 -0
  70. data/test/app/helpers/photos_helper.rb +2 -0
  71. data/test/app/helpers/posts_helper.rb +2 -0
  72. data/test/app/helpers/projects_helper.rb +2 -0
  73. data/test/app/helpers/somethings_helper.rb +2 -0
  74. data/test/app/helpers/tags_helper.rb +2 -0
  75. data/test/app/helpers/users_helper.rb +2 -0
  76. data/test/app/models/account.rb +3 -0
  77. data/test/app/models/comment.rb +3 -0
  78. data/test/app/models/option.rb +3 -0
  79. data/test/app/models/photo.rb +4 -0
  80. data/test/app/models/post.rb +3 -0
  81. data/test/app/models/product.rb +3 -0
  82. data/test/app/models/project.rb +2 -0
  83. data/test/app/models/something.rb +2 -0
  84. data/test/app/models/tag.rb +3 -0
  85. data/test/app/views/cms/options/edit.rhtml +17 -0
  86. data/test/app/views/cms/options/index.rhtml +20 -0
  87. data/test/app/views/cms/options/new.rhtml +16 -0
  88. data/test/app/views/cms/options/show.rhtml +8 -0
  89. data/test/app/views/cms/products/edit.rhtml +17 -0
  90. data/test/app/views/cms/products/index.rhtml +20 -0
  91. data/test/app/views/cms/products/new.rhtml +16 -0
  92. data/test/app/views/cms/products/show.rhtml +8 -0
  93. data/test/app/views/comments/edit.rhtml +27 -0
  94. data/test/app/views/comments/index.rhtml +24 -0
  95. data/test/app/views/comments/new.rhtml +26 -0
  96. data/test/app/views/comments/show.rhtml +18 -0
  97. data/test/app/views/layouts/application.rhtml +17 -0
  98. data/test/app/views/layouts/comments.rhtml +17 -0
  99. data/test/app/views/layouts/options.rhtml +17 -0
  100. data/test/app/views/layouts/people.rhtml +17 -0
  101. data/test/app/views/layouts/photos.rhtml +17 -0
  102. data/test/app/views/layouts/projects.rhtml +17 -0
  103. data/test/app/views/layouts/somethings.rhtml +17 -0
  104. data/test/app/views/layouts/tags.rhtml +17 -0
  105. data/test/app/views/people/edit.rhtml +17 -0
  106. data/test/app/views/people/index.rhtml +20 -0
  107. data/test/app/views/people/new.rhtml +16 -0
  108. data/test/app/views/people/show.rhtml +8 -0
  109. data/test/app/views/photos/edit.rhtml +17 -0
  110. data/test/app/views/photos/index.rhtml +20 -0
  111. data/test/app/views/photos/new.rhtml +16 -0
  112. data/test/app/views/photos/show.rhtml +8 -0
  113. data/test/app/views/posts/edit.rhtml +22 -0
  114. data/test/app/views/posts/index.rhtml +22 -0
  115. data/test/app/views/posts/new.rhtml +21 -0
  116. data/test/app/views/posts/show.rhtml +13 -0
  117. data/test/app/views/projects/edit.rhtml +17 -0
  118. data/test/app/views/projects/index.rhtml +20 -0
  119. data/test/app/views/projects/new.rhtml +16 -0
  120. data/test/app/views/projects/show.rhtml +8 -0
  121. data/test/app/views/somethings/edit.rhtml +17 -0
  122. data/test/app/views/somethings/index.rhtml +20 -0
  123. data/test/app/views/somethings/new.rhtml +16 -0
  124. data/test/app/views/somethings/show.rhtml +8 -0
  125. data/test/app/views/tags/edit.rhtml +17 -0
  126. data/test/app/views/tags/index.rhtml +20 -0
  127. data/test/app/views/tags/index.rjs +0 -0
  128. data/test/app/views/tags/new.rhtml +16 -0
  129. data/test/app/views/tags/show.rhtml +8 -0
  130. data/test/app/views/users/edit.rhtml +17 -0
  131. data/test/app/views/users/index.rhtml +20 -0
  132. data/test/app/views/users/new.rhtml +16 -0
  133. data/test/app/views/users/show.rhtml +8 -0
  134. data/test/config/boot.rb +109 -0
  135. data/test/config/database.yml +16 -0
  136. data/test/config/environment.rb +64 -0
  137. data/test/config/environments/development.rb +21 -0
  138. data/test/config/environments/test.rb +19 -0
  139. data/test/config/routes.rb +51 -0
  140. data/test/db/migrate/001_create_posts.rb +12 -0
  141. data/test/db/migrate/002_create_products.rb +11 -0
  142. data/test/db/migrate/003_create_comments.rb +13 -0
  143. data/test/db/migrate/004_create_options.rb +12 -0
  144. data/test/db/migrate/005_create_photos.rb +11 -0
  145. data/test/db/migrate/006_create_tags.rb +17 -0
  146. data/test/db/migrate/007_create_somethings.rb +11 -0
  147. data/test/db/migrate/008_create_accounts.rb +11 -0
  148. data/test/db/migrate/009_add_account_id_to_photos.rb +9 -0
  149. data/test/db/migrate/010_create_projects.rb +11 -0
  150. data/test/db/schema.rb +65 -0
  151. data/test/log/development.log +2918 -0
  152. data/test/log/test.log +135619 -0
  153. data/test/log/thin.log +12 -0
  154. data/test/script/console +3 -0
  155. data/test/script/destroy +3 -0
  156. data/test/script/generate +3 -0
  157. data/test/script/server +3 -0
  158. data/test/test/fixtures/accounts.yml +7 -0
  159. data/test/test/fixtures/comments.yml +11 -0
  160. data/test/test/fixtures/options.yml +9 -0
  161. data/test/test/fixtures/photos.yml +9 -0
  162. data/test/test/fixtures/photos_tags.yml +3 -0
  163. data/test/test/fixtures/posts.yml +9 -0
  164. data/test/test/fixtures/products.yml +7 -0
  165. data/test/test/fixtures/projects.yml +7 -0
  166. data/test/test/fixtures/somethings.yml +7 -0
  167. data/test/test/fixtures/tags.yml +7 -0
  168. data/test/test/functional/cms/options_controller_test.rb +23 -0
  169. data/test/test/functional/cms/products_controller_test.rb +23 -0
  170. data/test/test/functional/comments_controller_test.rb +26 -0
  171. data/test/test/functional/people_controller_test.rb +34 -0
  172. data/test/test/functional/photos_controller_test.rb +130 -0
  173. data/test/test/functional/posts_controller_test.rb +34 -0
  174. data/test/test/functional/projects_controller_test.rb +18 -0
  175. data/test/test/functional/somethings_controller_test.rb +28 -0
  176. data/test/test/functional/tags_controller_test.rb +64 -0
  177. data/test/test/functional/users_controller_test.rb +24 -0
  178. data/test/test/test_helper.rb +12 -0
  179. data/test/test/unit/accessors_test.rb +110 -0
  180. data/test/test/unit/account_test.rb +7 -0
  181. data/test/test/unit/action_options_test.rb +109 -0
  182. data/test/test/unit/base_test.rb +11 -0
  183. data/test/test/unit/comment_test.rb +10 -0
  184. data/test/test/unit/failable_action_options_test.rb +77 -0
  185. data/test/test/unit/helpers/current_objects_test.rb +127 -0
  186. data/test/test/unit/helpers/internal_test.rb +106 -0
  187. data/test/test/unit/helpers/nested_test.rb +82 -0
  188. data/test/test/unit/helpers/urls_test.rb +71 -0
  189. data/test/test/unit/helpers_test.rb +25 -0
  190. data/test/test/unit/option_test.rb +10 -0
  191. data/test/test/unit/photo_test.rb +10 -0
  192. data/test/test/unit/post_test.rb +10 -0
  193. data/test/test/unit/project_test.rb +10 -0
  194. data/test/test/unit/response_collector_test.rb +49 -0
  195. data/test/test/unit/something_test.rb +10 -0
  196. data/test/test/unit/tag_test.rb +10 -0
  197. data/test/test/unit/urligence_test.rb +203 -0
  198. data/test/vendor/plugins/shoulda/Rakefile +32 -0
  199. data/test/vendor/plugins/shoulda/bin/convert_to_should_syntax +40 -0
  200. data/test/vendor/plugins/shoulda/init.rb +3 -0
  201. data/test/vendor/plugins/shoulda/lib/shoulda.rb +43 -0
  202. data/test/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb +580 -0
  203. data/test/vendor/plugins/shoulda/lib/shoulda/color.rb +77 -0
  204. data/test/vendor/plugins/shoulda/lib/shoulda/controller_tests/controller_tests.rb +467 -0
  205. data/test/vendor/plugins/shoulda/lib/shoulda/controller_tests/formats/html.rb +201 -0
  206. data/test/vendor/plugins/shoulda/lib/shoulda/controller_tests/formats/xml.rb +170 -0
  207. data/test/vendor/plugins/shoulda/lib/shoulda/gem/proc_extensions.rb +14 -0
  208. data/test/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb +239 -0
  209. data/test/vendor/plugins/shoulda/lib/shoulda/general.rb +118 -0
  210. data/test/vendor/plugins/shoulda/lib/shoulda/private_helpers.rb +22 -0
  211. metadata +312 -0
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ # Test::Unit::UI::VERBOSE
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'lib'
9
+ t.pattern = 'test/{unit,functional,other}/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ Rake::RDocTask.new { |rdoc|
14
+ rdoc.rdoc_dir = 'doc'
15
+ rdoc.title = "Shoulda -- Making tests easy on the fingers and eyes"
16
+ rdoc.options << '--line-numbers' << '--inline-source'
17
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
18
+ rdoc.rdoc_files.include('README', 'lib/**/*.rb')
19
+ }
20
+
21
+ desc 'Update documentation on website'
22
+ task :sync_docs => 'rdoc' do
23
+ `rsync -ave ssh doc/ dev@dev.thoughtbot.com:/home/dev/www/dev.thoughtbot.com/shoulda`
24
+ end
25
+
26
+ desc 'Default: run tests.'
27
+ task :default => ['test']
28
+
29
+ Dir['tasks/*.rake'].each do |f|
30
+ load f
31
+ end
32
+
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+
4
+ def usage(msg = nil)
5
+ puts "Error: #{msg}" if msg
6
+ puts if msg
7
+ puts "Usage: #{File.basename(__FILE__)} normal_test_file.rb"
8
+ puts
9
+ puts "Will convert an existing test file with names like "
10
+ puts
11
+ puts " def test_should_do_stuff"
12
+ puts " ..."
13
+ puts " end"
14
+ puts
15
+ puts "to one using the new syntax: "
16
+ puts
17
+ puts " should \"be super cool\" do"
18
+ puts " ..."
19
+ puts " end"
20
+ puts
21
+ puts "A copy of the old file will be left under /tmp/ in case this script just seriously screws up"
22
+ puts
23
+ exit (msg ? 2 : 0)
24
+ end
25
+
26
+ usage("Wrong number of arguments.") unless ARGV.size == 1
27
+ usage("This system doesn't have a /tmp directory. wtf?") unless File.directory?('/tmp')
28
+
29
+ file = ARGV.shift
30
+ tmpfile = "/tmp/#{File.basename(file)}"
31
+ usage("File '#{file}' doesn't exist") unless File.exists?(file)
32
+
33
+ FileUtils.cp(file, tmpfile)
34
+ contents = File.read(tmpfile)
35
+ contents.gsub!(/def test_should_(.*)\s*$/, 'should "\1" do')
36
+ contents.gsub!(/def test_(.*)\s*$/, 'should "RENAME ME: test \1" do')
37
+ contents.gsub!(/should ".*" do$/) {|line| line.tr!('_', ' ')}
38
+ File.open(file, 'w') { |f| f.write(contents) }
39
+
40
+ puts "File '#{file}' has been converted to 'should' syntax. Old version has been stored in '#{tmpfile}'"
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'shoulda'
@@ -0,0 +1,43 @@
1
+ require 'shoulda/gem/shoulda'
2
+ require 'shoulda/private_helpers'
3
+ require 'shoulda/general'
4
+ require 'shoulda/active_record_helpers'
5
+ require 'shoulda/controller_tests/controller_tests.rb'
6
+ require 'yaml'
7
+
8
+ shoulda_options = {}
9
+
10
+ possible_config_paths = []
11
+ possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"]
12
+ possible_config_paths << "shoulda.conf"
13
+ possible_config_paths << File.join("test", "shoulda.conf")
14
+ possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT)
15
+
16
+ possible_config_paths.each do |config_file|
17
+ if File.exists? config_file
18
+ shoulda_options = YAML.load_file(config_file).symbolize_keys
19
+ break
20
+ end
21
+ end
22
+
23
+ require 'shoulda/color' if shoulda_options[:color]
24
+
25
+ module Test # :nodoc: all
26
+ module Unit
27
+ class TestCase
28
+
29
+ include ThoughtBot::Shoulda::General
30
+ include ThoughtBot::Shoulda::Controller
31
+
32
+ extend ThoughtBot::Shoulda::ActiveRecord
33
+ end
34
+ end
35
+ end
36
+
37
+ module ActionController #:nodoc: all
38
+ module Integration
39
+ class Session
40
+ include ThoughtBot::Shoulda::General
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,580 @@
1
+ module ThoughtBot # :nodoc:
2
+ module Shoulda # :nodoc:
3
+ # = Macro test helpers for your active record models
4
+ #
5
+ # These helpers will test most of the validations and associations for your ActiveRecord models.
6
+ #
7
+ # class UserTest < Test::Unit::TestCase
8
+ # should_require_attributes :name, :phone_number
9
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
10
+ # should_allow_values_for :phone_number, "(123) 456-7890"
11
+ #
12
+ # should_protect_attributes :password
13
+ #
14
+ # should_have_one :profile
15
+ # should_have_many :dogs
16
+ # should_have_many :messes, :through => :dogs
17
+ # should_belong_to :lover
18
+ # end
19
+ #
20
+ # For all of these helpers, the last parameter may be a hash of options.
21
+ #
22
+ module ActiveRecord
23
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
24
+ #
25
+ # Options:
26
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
27
+ # Regexp or string. Default = <tt>/blank/</tt>
28
+ #
29
+ # Example:
30
+ # should_require_attributes :name, :phone_number
31
+ #
32
+ def should_require_attributes(*attributes)
33
+ message = get_options!(attributes, :message)
34
+ message ||= /blank/
35
+ klass = model_class
36
+
37
+ attributes.each do |attribute|
38
+ should "require #{attribute} to be set" do
39
+ object = klass.new
40
+ object.send("#{attribute}=", nil)
41
+ assert !object.valid?, "#{klass.name} does not require #{attribute}."
42
+ assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}."
43
+ assert_contains(object.errors.on(attribute), message)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
49
+ # Requires an existing record
50
+ #
51
+ # Options:
52
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
53
+ # Regexp or string. Default = <tt>/taken/</tt>
54
+ #
55
+ # Example:
56
+ # should_require_unique_attributes :keyword, :username
57
+ #
58
+ def should_require_unique_attributes(*attributes)
59
+ message, scope = get_options!(attributes, :message, :scoped_to)
60
+ message ||= /taken/
61
+
62
+ klass = model_class
63
+ attributes.each do |attribute|
64
+ attribute = attribute.to_sym
65
+ should "require unique value for #{attribute}#{" scoped to #{scope}" if scope}" do
66
+ assert existing = klass.find(:first), "Can't find first #{klass}"
67
+ object = klass.new
68
+
69
+ object.send(:"#{attribute}=", existing.send(attribute))
70
+ if scope
71
+ assert_respond_to object, :"#{scope}=", "#{klass.name} doesn't seem to have a #{scope} attribute."
72
+ object.send(:"#{scope}=", existing.send(scope))
73
+ end
74
+
75
+ assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}."
76
+ assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}."
77
+
78
+ assert_contains(object.errors.on(attribute), message)
79
+
80
+ # Now test that the object is valid when changing the scoped attribute
81
+ # TODO: There is a chance that we could change the scoped field
82
+ # to a value that's already taken. An alternative implementation
83
+ # could actually find all values for scope and create a unique
84
+ # one.
85
+ if scope
86
+ # Assume the scope is a foreign key if the field is nil
87
+ object.send(:"#{scope}=", existing.send(scope).nil? ? 1 : existing.send(scope).next)
88
+ object.errors.clear
89
+ object.valid?
90
+ assert_does_not_contain(object.errors.on(attribute), message,
91
+ "after :#{scope} set to #{object.send(scope.to_sym)}")
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Ensures that the attribute cannot be set on mass update.
98
+ # Requires an existing record.
99
+ #
100
+ # should_protect_attributes :password, :admin_flag
101
+ #
102
+ def should_protect_attributes(*attributes)
103
+ get_options!(attributes)
104
+ klass = model_class
105
+
106
+ attributes.each do |attribute|
107
+ attribute = attribute.to_sym
108
+ should "protect #{attribute} from mass updates" do
109
+ protected = klass.protected_attributes || []
110
+ accessible = klass.accessible_attributes || []
111
+
112
+ assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s),
113
+ (accessible.empty? ?
114
+ "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." :
115
+ "#{klass} has made #{attribute} accessible")
116
+ end
117
+ end
118
+ end
119
+
120
+ # Ensures that the attribute cannot be set to the given values
121
+ # Requires an existing record
122
+ #
123
+ # Options:
124
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
125
+ # Regexp or string. Default = <tt>/invalid/</tt>
126
+ #
127
+ # Example:
128
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
129
+ #
130
+ def should_not_allow_values_for(attribute, *bad_values)
131
+ message = get_options!(bad_values, :message)
132
+ message ||= /invalid/
133
+ klass = model_class
134
+ bad_values.each do |v|
135
+ should "not allow #{attribute} to be set to #{v.inspect}" do
136
+ assert object = klass.find(:first), "Can't find first #{klass}"
137
+ object.send("#{attribute}=", v)
138
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
139
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
140
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{v}\"")
141
+ end
142
+ end
143
+ end
144
+
145
+ # Ensures that the attribute can be set to the given values.
146
+ # Requires an existing record
147
+ #
148
+ # Example:
149
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
150
+ #
151
+ def should_allow_values_for(attribute, *good_values)
152
+ get_options!(good_values)
153
+ klass = model_class
154
+ good_values.each do |v|
155
+ should "allow #{attribute} to be set to #{v.inspect}" do
156
+ assert object = klass.find(:first), "Can't find first #{klass}"
157
+ object.send("#{attribute}=", v)
158
+ object.save
159
+ assert_nil object.errors.on(attribute)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Ensures that the length of the attribute is in the given range
165
+ # Requires an existing record
166
+ #
167
+ # Options:
168
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
169
+ # Regexp or string. Default = <tt>/short/</tt>
170
+ # * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
171
+ # Regexp or string. Default = <tt>/long/</tt>
172
+ #
173
+ # Example:
174
+ # should_ensure_length_in_range :password, (6..20)
175
+ #
176
+ def should_ensure_length_in_range(attribute, range, opts = {})
177
+ short_message, long_message = get_options!([opts], :short_message, :long_message)
178
+ short_message ||= /short/
179
+ long_message ||= /long/
180
+
181
+ klass = model_class
182
+ min_length = range.first
183
+ max_length = range.last
184
+ same_length = (min_length == max_length)
185
+
186
+ if min_length > 0
187
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
188
+ min_value = "x" * (min_length - 1)
189
+ assert object = klass.find(:first), "Can't find first #{klass}"
190
+ object.send("#{attribute}=", min_value)
191
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
192
+ assert object.errors.on(attribute),
193
+ "There are no errors set on #{attribute} after being set to \"#{min_value}\""
194
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
195
+ end
196
+ end
197
+
198
+ if min_length > 0
199
+ should "allow #{attribute} to be exactly #{min_length} chars long" do
200
+ min_value = "x" * min_length
201
+ assert object = klass.find(:first), "Can't find first #{klass}"
202
+ object.send("#{attribute}=", min_value)
203
+ object.save
204
+ assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
205
+ end
206
+ end
207
+
208
+ should "not allow #{attribute} to be more than #{max_length} chars long" do
209
+ max_value = "x" * (max_length + 1)
210
+ assert object = klass.find(:first), "Can't find first #{klass}"
211
+ object.send("#{attribute}=", max_value)
212
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
213
+ assert object.errors.on(attribute),
214
+ "There are no errors set on #{attribute} after being set to \"#{max_value}\""
215
+ assert_contains(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
216
+ end
217
+
218
+ unless same_length
219
+ should "allow #{attribute} to be exactly #{max_length} chars long" do
220
+ max_value = "x" * max_length
221
+ assert object = klass.find(:first), "Can't find first #{klass}"
222
+ object.send("#{attribute}=", max_value)
223
+ object.save
224
+ assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
225
+ end
226
+ end
227
+ end
228
+
229
+ # Ensures that the length of the attribute is at least a certain length
230
+ # Requires an existing record
231
+ #
232
+ # Options:
233
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
234
+ # Regexp or string. Default = <tt>/short/</tt>
235
+ #
236
+ # Example:
237
+ # should_ensure_length_at_least :name, 3
238
+ #
239
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
240
+ short_message = get_options!([opts], :short_message)
241
+ short_message ||= /short/
242
+
243
+ klass = model_class
244
+
245
+ if min_length > 0
246
+ min_value = "x" * (min_length - 1)
247
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
248
+ assert object = klass.find(:first), "Can't find first #{klass}"
249
+ object.send("#{attribute}=", min_value)
250
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
251
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
252
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
253
+ end
254
+ end
255
+ should "allow #{attribute} to be at least #{min_length} chars long" do
256
+ valid_value = "x" * (min_length)
257
+ assert object = klass.find(:first), "Can't find first #{klass}"
258
+ object.send("#{attribute}=", valid_value)
259
+ assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\""
260
+ end
261
+ end
262
+
263
+ # Ensure that the attribute is in the range specified
264
+ # Requires an existing record
265
+ #
266
+ # Options:
267
+ # * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
268
+ # Regexp or string. Default = <tt>/included/</tt>
269
+ # * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
270
+ # Regexp or string. Default = <tt>/included/</tt>
271
+ #
272
+ # Example:
273
+ # should_ensure_value_in_range :age, (0..100)
274
+ #
275
+ def should_ensure_value_in_range(attribute, range, opts = {})
276
+ low_message, high_message = get_options!([opts], :low_message, :high_message)
277
+ low_message ||= /included/
278
+ high_message ||= /included/
279
+
280
+ klass = model_class
281
+ min = range.first
282
+ max = range.last
283
+
284
+ should "not allow #{attribute} to be less than #{min}" do
285
+ v = min - 1
286
+ assert object = klass.find(:first), "Can't find first #{klass}"
287
+ object.send("#{attribute}=", v)
288
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
289
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
290
+ assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
291
+ end
292
+
293
+ should "allow #{attribute} to be #{min}" do
294
+ v = min
295
+ assert object = klass.find(:first), "Can't find first #{klass}"
296
+ object.send("#{attribute}=", v)
297
+ object.save
298
+ assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
299
+ end
300
+
301
+ should "not allow #{attribute} to be more than #{max}" do
302
+ v = max + 1
303
+ assert object = klass.find(:first), "Can't find first #{klass}"
304
+ object.send("#{attribute}=", v)
305
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
306
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
307
+ assert_contains(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
308
+ end
309
+
310
+ should "allow #{attribute} to be #{max}" do
311
+ v = max
312
+ assert object = klass.find(:first), "Can't find first #{klass}"
313
+ object.send("#{attribute}=", v)
314
+ object.save
315
+ assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
316
+ end
317
+ end
318
+
319
+ # Ensure that the attribute is numeric
320
+ # Requires an existing record
321
+ #
322
+ # Options:
323
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
324
+ # Regexp or string. Default = <tt>/number/</tt>
325
+ #
326
+ # Example:
327
+ # should_only_allow_numeric_values_for :age
328
+ #
329
+ def should_only_allow_numeric_values_for(*attributes)
330
+ message = get_options!(attributes, :message)
331
+ message ||= /number/
332
+ klass = model_class
333
+ attributes.each do |attribute|
334
+ attribute = attribute.to_sym
335
+ should "only allow numeric values for #{attribute}" do
336
+ assert object = klass.find(:first), "Can't find first #{klass}"
337
+ object.send(:"#{attribute}=", "abcd")
338
+ assert !object.valid?, "Instance is still valid"
339
+ assert_contains(object.errors.on(attribute), message)
340
+ end
341
+ end
342
+ end
343
+
344
+ # Ensures that the has_many relationship exists. Will also test that the
345
+ # associated table has the required columns. Works with polymorphic
346
+ # associations.
347
+ #
348
+ # Options:
349
+ # * <tt>:through</tt> - association name for <tt>has_many :through</tt>
350
+ #
351
+ # Example:
352
+ # should_have_many :friends
353
+ # should_have_many :enemies, :through => :friends
354
+ #
355
+ def should_have_many(*associations)
356
+ through = get_options!(associations, :through)
357
+ klass = model_class
358
+ associations.each do |association|
359
+ name = "have many #{association}"
360
+ name += " through #{through}" if through
361
+ should name do
362
+ reflection = klass.reflect_on_association(association)
363
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
364
+ assert_equal :has_many, reflection.macro
365
+
366
+ if through
367
+ through_reflection = klass.reflect_on_association(through)
368
+ assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
369
+ assert_equal(through, reflection.options[:through])
370
+ end
371
+
372
+ unless reflection.options[:through]
373
+ # This is not a through association, so check for the existence of the foreign key on the other table
374
+ if reflection.options[:foreign_key]
375
+ fk = reflection.options[:foreign_key]
376
+ elsif reflection.options[:as]
377
+ fk = reflection.options[:as].to_s.foreign_key
378
+ else
379
+ fk = reflection.primary_key_name
380
+ end
381
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
382
+ assert associated_klass.column_names.include?(fk.to_s), "#{associated_klass.name} does not have a #{fk} foreign key."
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ # Ensure that the has_one relationship exists. Will also test that the
389
+ # associated table has the required columns. Works with polymorphic
390
+ # associations.
391
+ #
392
+ # Example:
393
+ # should_have_one :god # unless hindu
394
+ #
395
+ def should_have_one(*associations)
396
+ get_options!(associations)
397
+ klass = model_class
398
+ associations.each do |association|
399
+ should "have one #{association}" do
400
+ reflection = klass.reflect_on_association(association)
401
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
402
+ assert_equal :has_one, reflection.macro
403
+
404
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
405
+
406
+ if reflection.options[:foreign_key]
407
+ fk = reflection.options[:foreign_key]
408
+ elsif reflection.options[:as]
409
+ fk = reflection.options[:as].to_s.foreign_key
410
+ fk_type = fk.gsub(/_id$/, '_type')
411
+ assert associated_klass.column_names.include?(fk_type),
412
+ "#{associated_klass.name} does not have a #{fk_type} column."
413
+ else
414
+ fk = klass.name.foreign_key
415
+ end
416
+ assert associated_klass.column_names.include?(fk.to_s),
417
+ "#{associated_klass.name} does not have a #{fk} foreign key."
418
+ end
419
+ end
420
+ end
421
+
422
+ # Ensures that the has_and_belongs_to_many relationship exists, and that the join
423
+ # table is in place.
424
+ #
425
+ # should_have_and_belong_to_many :posts, :cars
426
+ #
427
+ def should_have_and_belong_to_many(*associations)
428
+ get_options!(associations)
429
+ klass = model_class
430
+
431
+ associations.each do |association|
432
+ should "should have and belong to many #{association}" do
433
+ reflection = klass.reflect_on_association(association)
434
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
435
+ assert_equal :has_and_belongs_to_many, reflection.macro
436
+ table = reflection.options[:join_table]
437
+ assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist"
438
+ end
439
+ end
440
+ end
441
+
442
+ # Ensure that the belongs_to relationship exists.
443
+ #
444
+ # should_belong_to :parent
445
+ #
446
+ def should_belong_to(*associations)
447
+ get_options!(associations)
448
+ klass = model_class
449
+ associations.each do |association|
450
+ should "belong_to #{association}" do
451
+ reflection = klass.reflect_on_association(association)
452
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
453
+ assert_equal :belongs_to, reflection.macro
454
+
455
+ unless reflection.options[:polymorphic]
456
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
457
+ fk = reflection.options[:foreign_key] || reflection.primary_key_name
458
+ assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
459
+ end
460
+ end
461
+ end
462
+ end
463
+
464
+ # Ensure that the given class methods are defined on the model.
465
+ #
466
+ # should_have_class_methods :find, :destroy
467
+ #
468
+ def should_have_class_methods(*methods)
469
+ get_options!(methods)
470
+ klass = model_class
471
+ methods.each do |method|
472
+ should "respond to class method ##{method}" do
473
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
474
+ end
475
+ end
476
+ end
477
+
478
+ # Ensure that the given instance methods are defined on the model.
479
+ #
480
+ # should_have_instance_methods :email, :name, :name=
481
+ #
482
+ def should_have_instance_methods(*methods)
483
+ get_options!(methods)
484
+ klass = model_class
485
+ methods.each do |method|
486
+ should "respond to instance method ##{method}" do
487
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
488
+ end
489
+ end
490
+ end
491
+
492
+ # Ensure that the given columns are defined on the models backing SQL table.
493
+ #
494
+ # should_have_db_columns :id, :email, :name, :created_at
495
+ #
496
+ def should_have_db_columns(*columns)
497
+ column_type = get_options!(columns, :type)
498
+ klass = model_class
499
+ columns.each do |name|
500
+ test_name = "have column #{name}"
501
+ test_name += " of type #{column_type}" if column_type
502
+ should test_name do
503
+ column = klass.columns.detect {|c| c.name == name.to_s }
504
+ assert column, "#{klass.name} does not have column #{name}"
505
+ end
506
+ end
507
+ end
508
+
509
+ # Ensure that the given column is defined on the models backing SQL table. The options are the same as
510
+ # the instance variables defined on the column definition: :precision, :limit, :default, :null,
511
+ # :primary, :type, :scale, and :sql_type.
512
+ #
513
+ # should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
514
+ # :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
515
+ #
516
+ def should_have_db_column(name, opts = {})
517
+ klass = model_class
518
+ test_name = "have column named :#{name}"
519
+ test_name += " with options " + opts.inspect unless opts.empty?
520
+ should test_name do
521
+ column = klass.columns.detect {|c| c.name == name.to_s }
522
+ assert column, "#{klass.name} does not have column #{name}"
523
+ opts.each do |k, v|
524
+ assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
525
+ end
526
+ end
527
+ end
528
+
529
+ # Ensures that there are DB indices on the given columns or tuples of columns.
530
+ # Also aliased to should_have_index for readability
531
+ #
532
+ # should_have_indices :email, :name, [:commentable_type, :commentable_id]
533
+ # should_have_index :age
534
+ #
535
+ def should_have_indices(*columns)
536
+ table = model_class.name.tableize
537
+ indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
538
+
539
+ columns.each do |column|
540
+ should "have index on #{table} for #{column.inspect}" do
541
+ columns = [column].flatten.map(&:to_s)
542
+ assert_contains(indices, columns)
543
+ end
544
+ end
545
+ end
546
+
547
+ alias_method :should_have_index, :should_have_indices
548
+
549
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
550
+ #
551
+ # Options:
552
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
553
+ # Regexp or string. Default = <tt>/must be accepted/</tt>
554
+ #
555
+ # Example:
556
+ # should_require_acceptance_of :eula
557
+ #
558
+ def should_require_acceptance_of(*attributes)
559
+ message = get_options!(attributes, :message)
560
+ message ||= /must be accepted/
561
+ klass = model_class
562
+
563
+ attributes.each do |attribute|
564
+ should "require #{attribute} to be accepted" do
565
+ object = klass.new
566
+ object.send("#{attribute}=", false)
567
+
568
+ assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}."
569
+ assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}."
570
+ assert_contains(object.errors.on(attribute), message)
571
+ end
572
+ end
573
+ end
574
+
575
+ private
576
+
577
+ include ThoughtBot::Shoulda::Private
578
+ end
579
+ end
580
+ end