summaryrefslogtreecommitdiff
path: root/test/models
diff options
context:
space:
mode:
Diffstat (limited to 'test/models')
-rw-r--r--test/models/concerns/rrule_humanizer_test.rb25
-rw-r--r--test/models/helpers/node_actions_helper_test.rb146
-rw-r--r--test/models/node_action_test.rb16
-rw-r--r--test/models/node_test.rb101
-rw-r--r--test/models/node_trash_test.rb168
-rw-r--r--test/models/page_test.rb35
-rw-r--r--test/models/user_test.rb72
7 files changed, 563 insertions, 0 deletions
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb
index 279ff73..d747171 100644
--- a/test/models/concerns/rrule_humanizer_test.rb
+++ b/test/models/concerns/rrule_humanizer_test.rb
@@ -47,6 +47,24 @@ class RruleHumanizerTest < ActiveSupport::TestCase
47 assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en) 47 assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en)
48 end 48 end
49 49
50 test "monthly selected weeks" do
51 assert_equal "Jeden ersten, zweiten und fünften Dienstag im Monat",
52 humanize("FREQ=MONTHLY;BYDAY=1TU,2TU,5TU")
53 assert_equal "Every first, second and fifth Tuesday of the month",
54 humanize("FREQ=MONTHLY;BYDAY=1TU,2TU,5TU", :en)
55 end
56
57 test "monthly fifth weekday" do
58 assert_equal "Jeden fünften Dienstag im Monat", humanize("FREQ=MONTHLY;BYDAY=5TU")
59 assert_equal "Every fifth Tuesday of the month", humanize("FREQ=MONTHLY;BYDAY=5TU", :en)
60 end
61
62 test "monthly weekday excluding one ordinal" do
63 rrule = "FREQ=MONTHLY;BYDAY=1TU,3TU,4TU,5TU"
64 assert_equal "Jeden Dienstag im Monat, außer dem zweiten Dienstag", humanize(rrule)
65 assert_equal "Every Tuesday of the month, except the second Tuesday", humanize(rrule, :en)
66 end
67
50 test "monthly no byday" do 68 test "monthly no byday" do
51 assert_equal "Monatlich", humanize("FREQ=MONTHLY") 69 assert_equal "Monatlich", humanize("FREQ=MONTHLY")
52 assert_equal "Monthly", humanize("FREQ=MONTHLY", :en) 70 assert_equal "Monthly", humanize("FREQ=MONTHLY", :en)
@@ -96,4 +114,11 @@ class RruleHumanizerTest < ActiveSupport::TestCase
96 test "wday_abbr falls back to :de for an unrecognized locale" do 114 test "wday_abbr falls back to :de for an unrecognized locale" do
97 assert_equal "Mo", RruleHumanizer.wday_abbr(Time.parse("2026-07-06"), :fr) 115 assert_equal "Mo", RruleHumanizer.wday_abbr(Time.parse("2026-07-06"), :fr)
98 end 116 end
117
118 test "monthly two selected weeks" do
119 assert_equal "Jeden ersten und dritten Dienstag im Monat",
120 humanize("FREQ=MONTHLY;BYDAY=1TU,3TU")
121 assert_equal "Every first and third Tuesday of the month",
122 humanize("FREQ=MONTHLY;BYDAY=1TU,3TU", :en)
123 end
99end 124end
diff --git a/test/models/helpers/node_actions_helper_test.rb b/test/models/helpers/node_actions_helper_test.rb
new file mode 100644
index 0000000..1b72ec9
--- /dev/null
+++ b/test/models/helpers/node_actions_helper_test.rb
@@ -0,0 +1,146 @@
1require 'test_helper'
2
3class NodeActionsHelperTest < ActionView::TestCase
4 def setup
5 @original_locale = I18n.locale
6 I18n.locale = :en
7 end
8
9 def default_url_options
10 { :locale => nil }
11 end
12
13 def icon(_name, **)
14 '<svg class="stub-icon"></svg>'.html_safe
15 end
16
17 def teardown
18 I18n.locale = @original_locale
19 end
20
21 def entry action, metadata = {}, node: nil, user: nil, page: nil
22 NodeAction.create!(:node => node, :user => user, :page => page,
23 :action => action, :occurred_at => Time.now,
24 :metadata => { "username" => "quentin",
25 "human_readable_node_name" => "Subject" }.merge(metadata))
26 end
27
28 test "publish renders the revision sentence, not the titles" do
29 out = action_summary(entry("publish",
30 { "via" => "draft", "title" => { "from" => "Old", "to" => "New" } }))
31
32 assert_includes out, "quentin"
33 assert_includes out, "new revision"
34 assert_not_includes out, "Old"
35 end
36
37 test "first publish uses its own sentence" do
38 out = action_summary(entry("publish",
39 { "via" => "draft", "title" => { "from" => nil, "to" => "New" } }))
40
41 assert_includes out, "for the first time"
42 end
43
44 test "rollback publishes get the rollback sentence" do
45 out = action_summary(entry("publish",
46 { "via" => "revision", "title" => { "from" => "Now", "to" => "Then" } }))
47
48 assert_includes out, "rolled"
49 end
50
51 test "move renders the path pair" do
52 out = action_summary(entry("move",
53 { "path" => { "from" => "a/b", "to" => "a/c" } }))
54
55 assert_includes out, "a/b"
56 assert_includes out, "a/c"
57 end
58
59 test "unknown verbs degrade to a generic sentence, never an error" do
60 out = action_summary(entry("frobnicate"))
61
62 assert_includes out, "frobnicate"
63 assert_includes out, "quentin"
64 end
65
66 test "dead references render as plain names from metadata, no links" do
67 out = action_summary(entry("publish",
68 { "title" => { "from" => "Old", "to" => "New" } }))
69
70 assert_not_includes out, "<a "
71 assert_includes out, "Subject"
72 end
73
74 test "metadata values are escaped" do
75 out = action_summary(entry("publish",
76 { "human_readable_node_name" => "<b>bold</b>" }))
77
78 assert_not_includes out, "<b>"
79 end
80
81 test "live associations upgrade names to links" do
82 out = action_summary(entry("publish",
83 { "title" => { "from" => "Old", "to" => "New" } },
84 :user => users(:quentin)))
85
86 assert_includes out, "<a "
87 end
88
89 test "details are guarded off when nothing but an unchanged title is present" do
90 unchanged = entry("publish", { "title" => { "from" => "Same", "to" => "Same" } })
91 changed = entry("publish", { "title" => { "from" => "Old", "to" => "New" } })
92
93 assert_not action_details?(unchanged)
94 assert action_details?(changed)
95 end
96
97 test "first publish with a byline names the author" do
98 out = action_summary(entry("publish",
99 { "title" => { "from" => nil, "to" => "New" },
100 "author" => { "from" => nil, "to" => "quentin" } }))
101
102 assert_includes out, "author"
103 end
104
105 test "create entries with their flat title render and are guarded off details" do
106 e = entry("create", { "title" => "Initial", "path" => "a/b" })
107
108 assert_includes action_summary(e), "a/b"
109 assert_not action_details?(e)
110 end
111
112 test "trash, restore, and destroy render their paths" do
113 trash = entry("trash", { "path" => { "from" => "club/old", "to" => "trash/old" } })
114 restore = entry("restore_from_trash", { "path" => { "from" => "trash/old", "to" => "club/new" } })
115 doomed = entry("destroy", { "path" => "trash/old" })
116
117 assert_includes action_summary(trash), "club/old"
118 assert_includes action_summary(restore), "club/new"
119 assert_includes action_summary(doomed), "trash/old"
120 end
121
122 test "revision lifecycle badges cover create, publish, rollback, and inference" do
123 created = entry("create", { "title" => "x", "path" => "a/b" })
124 published = entry("publish", { "via" => "draft", "title" => { "from" => nil, "to" => "x" } })
125 restored = entry("publish", { "via" => "revision", "title" => { "from" => "y", "to" => "x" } })
126 restored.update!(:inferred_from => "from_page_revision")
127
128 out = revision_lifecycle_badges([created, published, restored])
129
130 assert_includes out, "created"
131 assert_includes out, "published"
132 assert_includes out, "restored"
133 assert_includes out, "quentin"
134 assert_includes out, "node_action_inferred"
135 assert_includes out, "<span"
136 assert_not_includes out, "&lt;span"
137
138 assert_equal "", revision_lifecycle_badges(nil)
139 end
140
141 test "verb icons map known verbs, distinguish rollbacks, and fall back" do
142 assert_includes verb_icon(entry("trash", { "path" => { "from" => "a", "to" => "t/a" } })), "node_action_icon--trash"
143 assert_includes verb_icon(entry("publish", { "via" => "revision" })), "node_action_icon--history"
144 assert_includes verb_icon(entry("frobnicate")), "node_action_icon--circle-dashed"
145 end
146end
diff --git a/test/models/node_action_test.rb b/test/models/node_action_test.rb
index 24d8245..b177cca 100644
--- a/test/models/node_action_test.rb
+++ b/test/models/node_action_test.rb
@@ -18,6 +18,12 @@ class NodeActionTest < ActiveSupport::TestCase
18 assert_equal({ :title => { "from" => nil, "to" => "Erstausgabe" } }, diff) 18 assert_equal({ :title => { "from" => nil, "to" => "Erstausgabe" } }, diff)
19 end 19 end
20 20
21 test "first publish carries the byline when the page has one" do
22 diff = NodeAction.head_diff(nil, build_page(:user => users(:quentin)))
23
24 assert_equal({ "from" => nil, "to" => users(:quentin).login }, diff[:author])
25 end
26
21 test "title pair is always present, even when unchanged" do 27 test "title pair is always present, even when unchanged" do
22 diff = NodeAction.head_diff(build_page, build_page) 28 diff = NodeAction.head_diff(build_page, build_page)
23 29
@@ -109,4 +115,14 @@ class NodeActionTest < ActiveSupport::TestCase
109 115
110 assert_nil NodeAction.head_diff(old_page, new_page)[:translation_diff] 116 assert_nil NodeAction.head_diff(old_page, new_page)[:translation_diff]
111 end 117 end
118
119 test "record! passes provenance and historical timestamps through" do
120 long_ago = 2.years.ago
121 action = NodeAction.record!(:node => nil, :action => "publish",
122 :occurred_at => long_ago,
123 :inferred_from => "from_page_revision")
124
125 assert_equal "from_page_revision", action.inferred_from
126 assert_in_delta long_ago.to_f, action.occurred_at.to_f, 1.0
127 end
112end 128end
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index c8e49e5..ab66f81 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -761,4 +761,105 @@ class NodeTest < ActiveSupport::TestCase
761 assert_equal "Second", action.metadata.dig("title", "from") 761 assert_equal "Second", action.metadata.dig("title", "from")
762 assert_equal "First", action.metadata.dig("title", "to") 762 assert_equal "First", action.metadata.dig("title", "to")
763 end 763 end
764
765 test "default_template_name rejects values not present in the template directory" do
766 node = Node.root.children.build(:slug => "template_guard_test",
767 :default_template_name => "../../layouts/admin")
768 assert_not node.valid?
769
770 node.default_template_name = "standard_template"
771 assert node.valid?
772 end
773
774 test "destroying a node with children is refused" do
775 parent = Node.root.children.create!(:slug => "destroy_guard_parent")
776 parent.children.create!(:slug => "destroy_guard_child")
777
778 assert_no_difference "Node.count" do
779 assert_not parent.destroy
780 end
781 assert parent.errors[:base].any?
782 end
783
784 test "destroying a childless node leaves no orphaned pages or asset links" do
785 node = create_node_with_published_page
786 page_ids = node.pages.pluck(:id)
787 asset = Asset.create!(:name => "destroy cascade probe",
788 :upload_file_name => "test_image.png",
789 :upload_content_type => "image/png",
790 :upload_file_size => 49854,
791 :upload_updated_at => Time.current)
792 node.head.related_assets.create!(:asset_id => asset.id, :position => 1)
793
794 node.destroy
795
796 assert_equal 0, Page.where(:id => page_ids).count
797 assert_equal 0, RelatedAsset.where(:page_id => page_ids).count
798 end
799
800 test "destroying a node also removes its autosave page" do
801 node = create_node_with_published_page
802 node.lock_for_editing!(@user1)
803 node.autosave!({:title => "in flight"}, @user1)
804 autosave_id = node.autosave_id
805
806 node.destroy
807
808 assert_equal 0, Page.where(:id => autosave_id).count
809 end
810
811 test "Node.trash lazily creates the container exactly once" do
812 assert_difference "Node.count", 1 do
813 Node.trash
814 end
815 assert_no_difference "Node.count" do
816 assert_equal Node.trash, Node.trash
817 end
818 assert Node.trash.trash_node?
819 assert_not Node.trash.in_trash?
820 assert_equal "Trash", Globalize.with_locale(I18n.default_locale) { Node.trash.draft.title }
821 end
822
823 test "in_trash? walks the whole parent chain" do
824 child = Node.trash.children.create!(:slug => "trashed_thing")
825 grandchild = child.children.create!(:slug => "trashed_deeper")
826
827 assert child.in_trash?
828 assert grandchild.in_trash?
829 assert_not Node.root.children.create!(:slug => "living_thing").in_trash?
830 end
831
832 test "the reserved slug is refused for other root children, live and staged" do
833 Node.trash
834
835 assert_not Node.root.children.build(:slug => CccConventions::TRASH_SLUG).valid?
836 assert_not Node.root.children.build(:slug => "fine", :staged_slug => CccConventions::TRASH_SLUG).valid?
837 assert Node.trash.children.create!(:slug => "sub").children.build(:slug => CccConventions::TRASH_SLUG).valid?
838 end
839
840 test "the Trash node refuses rename, move, and destroy" do
841 trash = Node.trash
842 other = Node.root.children.create!(:slug => "not_the_trash")
843
844 trash.slug = "recycling"
845 assert_not trash.valid?
846
847 trash.reload.parent_id = other.id
848 assert_not trash.valid?
849
850 assert_no_difference "Node.count" do
851 assert_not trash.reload.destroy
852 end
853 end
854
855 test "a head cannot exist inside the Trash, and publishing there is refused" do
856 node = Node.trash.children.create!(:slug => "no_publish_here")
857 page = node.pages.create!
858
859 node.head = page
860 assert_not node.valid?
861
862 node.reload
863 assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft! }
864 end
764end 865end
diff --git a/test/models/node_trash_test.rb b/test/models/node_trash_test.rb
new file mode 100644
index 0000000..52069d7
--- /dev/null
+++ b/test/models/node_trash_test.rb
@@ -0,0 +1,168 @@
1require "test_helper"
2
3class NodeTrashTest < ActiveSupport::TestCase
4
5 def setup
6 @user1 = User.create!(:login => "trasher", :email => "t@example.com",
7 :password => "foobar", :password_confirmation => "foobar")
8 end
9
10 test "trash! demotes the head into the draft slot when no draft exists" do
11 node = create_node_with_published_page
12 former_head = node.head
13
14 node.trash!(@user1)
15 node.reload
16
17 assert_nil node.head_id
18 assert_equal former_head, node.draft
19 assert node.in_trash?
20 end
21
22 test "trash! keeps an existing draft; the former head stays a plain revision" do
23 node = create_node_with_published_page
24 draft = find_or_create_draft(node, @user1)
25 former_head = node.head
26
27 node.trash!(@user1)
28 node.reload
29
30 assert_nil node.head_id
31 assert_equal draft, node.draft
32 assert_includes node.pages, former_head
33 end
34
35 test "trash! demotes descendant heads and counts them in the log entry" do
36 parent = create_node_with_published_page
37 child = parent.children.create!(:slug => "trashed_child")
38 child.draft.update!(:title => "Child")
39 child.publish_draft!(@user1)
40
41 parent.trash!(@user1)
42
43 action = NodeAction.where(:action => "trash").last
44 assert_equal parent, action.node
45 assert_equal 2, action.metadata["demoted_heads"]
46 assert action.metadata["was_published"]
47 assert_nil child.reload.head_id
48 assert child.in_trash?
49 end
50
51 test "trash! logs one entry with the path pair and updates unique names" do
52 node = create_node_with_published_page
53 path_before = node.unique_name
54
55 assert_difference "NodeAction.count", 1 do
56 node.trash!(@user1)
57 end
58
59 action = NodeAction.last
60 assert_equal path_before, action.metadata.dig("path", "from")
61 assert_equal node.reload.unique_name, action.metadata.dig("path", "to")
62 assert action.metadata.dig("path", "to").start_with?(CccConventions::TRASH_SLUG)
63 end
64
65 test "trash! on an already trashed node is a logged-nothing no-op" do
66 node = create_node_with_published_page
67 node.trash!(@user1)
68
69 assert_no_difference "NodeAction.count" do
70 assert_nil node.reload.trash!(@user1)
71 end
72 end
73
74 test "restore_from_trash! reparents as drafts without republishing" do
75 node = create_node_with_published_page
76 node.trash!(@user1)
77 target = Node.root.children.create!(:slug => "restore_target")
78
79 node.reload.restore_from_trash!(target, @user1)
80 node.reload
81
82 assert_equal target, node.parent
83 assert_not node.in_trash?
84 assert_nil node.head_id
85 assert_not_nil node.draft_id
86 assert_equal "restore_from_trash", NodeAction.last.action
87 end
88
89 test "restore_from_trash! refuses targets inside the Trash and the Trash itself" do
90 node = create_node_with_published_page
91 node.trash!(@user1)
92 other_trashed = Node.trash.children.create!(:slug => "also_trashed")
93
94 assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(Node.trash, @user1) }
95 assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(other_trashed, @user1) }
96 end
97
98 test "destroy_from_trash! refuses nodes outside the Trash" do
99 node = create_node_with_published_page
100
101 assert_raises(ActiveRecord::RecordInvalid) { node.destroy_from_trash!(@user1) }
102 assert Node.exists?(node.id)
103 end
104
105 test "destroy_from_trash! removes the whole subtree bottom-up with one entry" do
106 parent = create_node_with_published_page
107 child = parent.children.create!(:slug => "doomed_child")
108 grandchild = child.children.create!(:slug => "doomed_grandchild")
109 page_ids = [parent, child, grandchild].flat_map { |n| n.pages.pluck(:id) }
110 parent.trash!(@user1)
111
112 assert_difference "NodeAction.count", 1 do
113 parent.reload.destroy_from_trash!(@user1)
114 end
115
116 assert_not Node.exists?(parent.id)
117 assert_not Node.exists?(child.id)
118 assert_not Node.exists?(grandchild.id)
119 assert_equal 0, Page.where(:id => page_ids).count
120
121 action = NodeAction.where(:action => "destroy").last
122 assert_equal 2, action.metadata["destroyed_descendants"]
123 assert_nil action.node_id
124 end
125
126 test "destroy_from_trash! logs an entry that survives the node" do
127 node = create_node_with_published_page
128 Globalize.with_locale(I18n.default_locale) { node.head.update!(:title => "Doomed") }
129 node.trash!(@user1)
130 final_path = node.reload.unique_name
131
132 node.destroy_from_trash!(@user1)
133
134 action = NodeAction.where(:action => "destroy").last
135 assert_nil action.node_id
136 assert_equal final_path, action.metadata["path"]
137 assert_equal "Doomed", action.subject_name
138 assert_equal @user1, action.user
139 end
140
141 test "suggested_restore_parent finds the old parent while it lives" do
142 parent = Node.root.children.create!(:slug => "old_home")
143 node = parent.children.create!(:slug => "comes_back")
144 node.trash!(@user1)
145
146 assert_equal parent, node.reload.suggested_restore_parent
147
148 parent.trash!(@user1)
149 assert_nil node.reload.suggested_restore_parent
150 end
151
152 test "suggested_restore_parent is root for trashed top-level nodes" do
153 node = Node.root.children.create!(:slug => "was_top_level")
154 node.trash!(@user1)
155
156 assert_equal Node.root, node.reload.suggested_restore_parent
157 end
158
159 test "trashed nodes and the Trash itself stay out of the drafts surfaces" do
160 Node.trash
161 node = create_node_with_published_page
162 node.trash!(@user1)
163
164 ids = Node.drafts_and_autosaves.pluck(:id)
165 assert_not_includes ids, Node.trash.id
166 assert_not_includes ids, node.id
167 end
168end
diff --git a/test/models/page_test.rb b/test/models/page_test.rb
index edb7c37..1f924f9 100644
--- a/test/models/page_test.rb
+++ b/test/models/page_test.rb
@@ -370,4 +370,39 @@ class PageTest < ActiveSupport::TestCase
370 assert en_entry[:changed] 370 assert en_entry[:changed]
371 refute en_entry[:exists_there] 371 refute en_entry[:exists_there]
372 end 372 end
373
374 test "aggregate ignores order_by values outside the allowlist" do
375 sql = Page.aggregate(:order_by => "pages.id; DROP TABLE pages--").to_sql
376
377 assert_not_includes sql, "DROP"
378 assert_includes sql, "pages.id ASC"
379 end
380
381 test "aggregate accepts allowlisted order columns, bare or prefixed" do
382 assert_includes Page.aggregate(:order_by => "published_at").to_sql,
383 "pages.published_at ASC"
384 assert_includes Page.aggregate(:order_by => "pages.published_at").to_sql,
385 "pages.published_at ASC"
386 end
387
388 test "template_name rejects values not present in the template directory" do
389 page = Page.create!(:title => "Template guard")
390
391 page.template_name = "../../partials/_article"
392 assert_not page.valid?
393
394 page.template_name = "standard_template"
395 assert page.valid?
396
397 page.template_name = ""
398 assert page.valid?
399 end
400
401 test "a stale legacy template_name does not block unrelated saves" do
402 page = Page.create!(:title => "Stale template")
403 page.update_column(:template_name, "long_deleted_template")
404
405 page.reload
406 assert page.update(:abstract => "still saveable")
407 end
373end 408end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 6e4d2d7..feccce2 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -54,6 +54,78 @@ class UserTest < ActiveSupport::TestCase
54 def test_should_authenticate_user 54 def test_should_authenticate_user
55 assert_equal users(:quentin), User.authenticate('quentin', 'monkey') 55 assert_equal users(:quentin), User.authenticate('quentin', 'monkey')
56 end 56 end
57
58 def test_should_not_authenticate_wrong_password
59 assert_nil User.authenticate("quentin", "wrong password")
60 end
61
62 def test_should_not_authenticate_unknown_user
63 assert_nil User.authenticate("nosuchuser", "monkey")
64 end
65
66 def test_user_with_crypted_password_is_migrated_on_login
67 user = users(:quentin)
68
69 assert_nil user.password_digest
70
71 assert User.authenticate("quentin", "monkey")
72
73 user.reload
74
75 assert_not_nil user.password_digest
76 assert_nil user.crypted_password
77 assert_nil user.salt
78 end
79
80 def test_new_user_uses_password_digest
81 user = create_user
82
83 assert_not_nil user.password_digest
84 assert_nil user.crypted_password
85 assert_nil user.salt
86
87 assert_equal user, User.authenticate("quire", "quire69")
88 end
89
90 def test_legacy_user_is_migrated_on_login
91 user = users(:quentin)
92
93 assert_nil user.password_digest
94 assert_not_nil user.crypted_password
95 assert_not_nil user.salt
96
97 assert_equal user, User.authenticate("quentin", "monkey")
98
99 user.reload
100
101 assert_not_nil user.password_digest
102 assert_nil user.crypted_password
103 assert_nil user.salt
104 end
105
106 def test_migrated_user_authenticates_using_password_digest
107 user = users(:quentin)
108
109 # Trigger automatic migration.
110 assert_equal user, User.authenticate("quentin", "monkey")
111
112 user.reload
113
114 assert_not_nil user.password_digest
115 assert_nil user.crypted_password
116 assert_nil user.salt
117
118 # Second login should now use password_digest.
119 assert_equal user, User.authenticate("quentin", "monkey")
120 end
121
122 def test_migrated_user_can_be_updated_without_password
123 user = users(:quentin)
124 assert_equal user, User.authenticate("quentin", "monkey")
125 user.reload
126
127 assert user.update(:email => "quentin@example.org")
128 end
57 129
58protected 130protected
59 def create_user(options = {}) 131 def create_user(options = {})