summaryrefslogtreecommitdiff
path: root/test/models
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-13 23:33:11 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-13 23:33:11 +0200
commit8eab68f2c5a3c126e2fec2ecdfa7ace87ce0937b (patch)
treef446ebc26a7707c7b64a937aa51a155df146c80a /test/models
parent42714c697273a7117c6b355fab26c8c35e336ad1 (diff)
parentcdf5d9941ca866d437612d2f863eac6eb0b3db12 (diff)
Merge branch 'erdgeist-revive-events'
Diffstat (limited to 'test/models')
-rw-r--r--test/models/concerns/rrule_humanizer_test.rb99
-rw-r--r--test/models/event_test.rb12
-rw-r--r--test/models/helpers/admin_helper_test.rb10
-rw-r--r--test/models/helpers/content_helper_test.rb4
-rw-r--r--test/models/helpers/datetime_helper_test.rb19
-rw-r--r--test/models/helpers/events_helper_test.rb21
-rw-r--r--test/models/helpers/nodes_helper_test.rb32
-rw-r--r--test/models/node_test.rb369
-rw-r--r--test/models/page_test.rb229
9 files changed, 781 insertions, 14 deletions
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb
new file mode 100644
index 0000000..279ff73
--- /dev/null
+++ b/test/models/concerns/rrule_humanizer_test.rb
@@ -0,0 +1,99 @@
1require 'test_helper'
2
3class RruleHumanizerTest < ActiveSupport::TestCase
4 def humanize(rrule, locale = :de)
5 Event.new(rrule: rrule).humanize_rrule(locale)
6 end
7
8 test "weekly single day" do
9 assert_equal "Jeden Dienstag", humanize("FREQ=WEEKLY;BYDAY=TU")
10 assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :en)
11 end
12
13 test "weekly two days" do
14 assert_equal "Jeden Mittwoch und Freitag", humanize("FREQ=WEEKLY;BYDAY=WE,FR")
15 assert_equal "Every Wednesday and Friday", humanize("FREQ=WEEKLY;BYDAY=WE,FR", :en)
16 end
17
18 test "weekly no byday" do
19 assert_equal "Wöchentlich", humanize("FREQ=WEEKLY")
20 assert_equal "Weekly", humanize("FREQ=WEEKLY", :en)
21 end
22
23 test "biweekly with day" do
24 assert_equal "Alle zwei Wochen donnerstags", humanize("FREQ=WEEKLY;INTERVAL=2;BYDAY=TH")
25 assert_equal "Every other Thursday", humanize("FREQ=WEEKLY;INTERVAL=2;BYDAY=TH", :en)
26 end
27
28 test "biweekly no day" do
29 assert_equal "Alle zwei Wochen", humanize("FREQ=WEEKLY;INTERVAL=2")
30 assert_equal "Every other week", humanize("FREQ=WEEKLY;INTERVAL=2", :en)
31 end
32
33 test "monthly nth weekday" do
34 assert_equal "Jeden ersten Dienstag im Monat", humanize("FREQ=MONTHLY;BYDAY=1TU")
35 assert_equal "Jeden zweiten Freitag im Monat", humanize("FREQ=MONTHLY;BYDAY=2FR")
36 assert_equal "Jeden dritten Sonntag im Monat", humanize("FREQ=MONTHLY;BYDAY=3SU")
37 assert_equal "Jeden letzten Mittwoch im Monat", humanize("FREQ=MONTHLY;BYDAY=-1WE")
38 end
39
40 test "monthly nth weekday english" do
41 assert_equal "Every first Tuesday of the month", humanize("FREQ=MONTHLY;BYDAY=1TU", :en)
42 assert_equal "Every last Wednesday of the month", humanize("FREQ=MONTHLY;BYDAY=-1WE", :en)
43 end
44
45 test "monthly second-to-last" do
46 assert_equal "Jeden vorletzten Donnerstag im Monat", humanize("FREQ=MONTHLY;BYDAY=-2TH")
47 assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en)
48 end
49
50 test "monthly no byday" do
51 assert_equal "Monatlich", humanize("FREQ=MONTHLY")
52 assert_equal "Monthly", humanize("FREQ=MONTHLY", :en)
53 end
54
55 test "monthly with single excluded month" do
56 assert_equal "Jeden letzten Donnerstag im Monat, außer im Dezember",
57 humanize("FREQ=MONTHLY;BYDAY=-1TH;BYMONTH=1,2,3,4,5,6,7,8,9,10,11")
58 assert_equal "Every last Thursday of the month, except in December",
59 humanize("FREQ=MONTHLY;BYDAY=-1TH;BYMONTH=1,2,3,4,5,6,7,8,9,10,11", :en)
60 end
61
62 test "monthly excluding january" do
63 assert_equal "Jeden zweiten Mittwoch im Monat, außer im Januar",
64 humanize("FREQ=MONTHLY;BYMONTH=2,3,4,5,6,7,8,9,10,11,12;BYDAY=2WE")
65 end
66
67 test "blank rrule returns nil" do
68 assert_nil humanize(nil)
69 assert_nil humanize("")
70 end
71
72 test "count and until are not guessed at" do
73 assert_nil humanize("FREQ=MONTHLY;BYDAY=1WE;COUNT=36")
74 assert_nil humanize("FREQ=MONTHLY;BYDAY=1WE;UNTIL=20050105T222222Z")
75 end
76
77 test "unrecognized freq returns nil" do
78 assert_nil humanize("FREQ=YEARLY;BYMONTH=12")
79 end
80
81 test "falls back to english for unknown locale" do
82 assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :fr)
83 end
84
85 test "wday_abbr returns the correct German abbreviation for each day" do
86 monday = Time.parse("2026-07-06") # confirmed Monday
87 assert_equal "Mo", RruleHumanizer.wday_abbr(monday, :de)
88 assert_equal "Di", RruleHumanizer.wday_abbr(monday + 1.day, :de)
89 assert_equal "Mi", RruleHumanizer.wday_abbr(monday + 2.days, :de)
90 assert_equal "Do", RruleHumanizer.wday_abbr(monday + 3.days, :de)
91 assert_equal "Fr", RruleHumanizer.wday_abbr(monday + 4.days, :de)
92 assert_equal "Sa", RruleHumanizer.wday_abbr(monday + 5.days, :de)
93 assert_equal "So", RruleHumanizer.wday_abbr(monday + 6.days, :de)
94 end
95
96 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)
98 end
99end
diff --git a/test/models/event_test.rb b/test/models/event_test.rb
index f310af8..e98605e 100644
--- a/test/models/event_test.rb
+++ b/test/models/event_test.rb
@@ -29,7 +29,6 @@ class EventTest < ActiveSupport::TestCase
29 :longitude => 13.378944, 29 :longitude => 13.378944,
30 :rrule => "FOOBAR", 30 :rrule => "FOOBAR",
31 :allday => false, 31 :allday => false,
32 :custom_rrule => false,
33 :node_id => @cal_node.id 32 :node_id => @cal_node.id
34 ) 33 )
35 end 34 end
@@ -44,7 +43,6 @@ class EventTest < ActiveSupport::TestCase
44 :longitude => 13.378944, 43 :longitude => 13.378944,
45 :rrule => nil, 44 :rrule => nil,
46 :allday => false, 45 :allday => false,
47 :custom_rrule => false,
48 :node_id => @cal_node.id 46 :node_id => @cal_node.id
49 ) 47 )
50 48
@@ -62,7 +60,6 @@ class EventTest < ActiveSupport::TestCase
62 :longitude => 13.378944, 60 :longitude => 13.378944,
63 :rrule => "FREQ=WEEKLY;INTERVAL=1", 61 :rrule => "FREQ=WEEKLY;INTERVAL=1",
64 :allday => false, 62 :allday => false,
65 :custom_rrule => false,
66 :node_id => @cal_node.id 63 :node_id => @cal_node.id
67 ) 64 )
68 65
@@ -74,17 +71,17 @@ class EventTest < ActiveSupport::TestCase
74 71
75 assert_equal "2009-12-24T15:23:42".to_time, scoped_occurrences[51].start_time 72 assert_equal "2009-12-24T15:23:42".to_time, scoped_occurrences[51].start_time
76 assert_equal "2009-12-24T20:05:23".to_time, scoped_occurrences[51].end_time 73 assert_equal "2009-12-24T20:05:23".to_time, scoped_occurrences[51].end_time
77 assert_equal @cal_node.event, scoped_occurrences[51].event 74 assert_equal @cal_node.events.first, scoped_occurrences[51].event
78 assert_equal @cal_node, scoped_occurrences[51].node 75 assert_equal @cal_node, scoped_occurrences[51].node
79 76
80 assert_equal "2009-03-19T15:23:42".to_time, scoped_occurrences[11].start_time 77 assert_equal "2009-03-19T15:23:42".to_time, scoped_occurrences[11].start_time
81 assert_equal "2009-03-19T20:05:23".to_time, scoped_occurrences[11].end_time 78 assert_equal "2009-03-19T20:05:23".to_time, scoped_occurrences[11].end_time
82 assert_equal @cal_node.event, scoped_occurrences[11].event 79 assert_equal @cal_node.events.first, scoped_occurrences[11].event
83 assert_equal @cal_node, scoped_occurrences[11].node 80 assert_equal @cal_node, scoped_occurrences[11].node
84 81
85 assert_equal "2009-01-01T15:23:42".to_time, scoped_occurrences[0].start_time 82 assert_equal "2009-01-01T15:23:42".to_time, scoped_occurrences[0].start_time
86 assert_equal "2009-01-01T20:05:23".to_time, scoped_occurrences[0].end_time 83 assert_equal "2009-01-01T20:05:23".to_time, scoped_occurrences[0].end_time
87 assert_equal @cal_node.event, scoped_occurrences[11].event 84 assert_equal @cal_node.events.first, scoped_occurrences[11].event
88 assert_equal @cal_node, scoped_occurrences[11].node 85 assert_equal @cal_node, scoped_occurrences[11].node
89 end 86 end
90 87
@@ -97,7 +94,6 @@ class EventTest < ActiveSupport::TestCase
97 :longitude => 13.378944, 94 :longitude => 13.378944,
98 :rrule => "FREQ=MONTHLY;INTERVAL=1;BYDAY=-1WE", 95 :rrule => "FREQ=MONTHLY;INTERVAL=1;BYDAY=-1WE",
99 :allday => false, 96 :allday => false,
100 :custom_rrule => true,
101 :node_id => @cal_node.id 97 :node_id => @cal_node.id
102 ) 98 )
103 99
@@ -111,4 +107,4 @@ class EventTest < ActiveSupport::TestCase
111 chaosradio_days = scoped_occurrences.map {|x| x.start_time.day} 107 chaosradio_days = scoped_occurrences.map {|x| x.start_time.day}
112 assert_equal expected_days, chaosradio_days 108 assert_equal expected_days, chaosradio_days
113 end 109 end
114end \ No newline at end of file 110end
diff --git a/test/models/helpers/admin_helper_test.rb b/test/models/helpers/admin_helper_test.rb
index 23d9f40..94e9861 100644
--- a/test/models/helpers/admin_helper_test.rb
+++ b/test/models/helpers/admin_helper_test.rb
@@ -1,4 +1,14 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class AdminHelperTest < ActionView::TestCase 3class AdminHelperTest < ActionView::TestCase
4 test "mtime_busted_path appends the file's real mtime as a query param" do
5 path = "/stylesheets/admin.css"
6 expected_mtime = File.mtime(Rails.public_path.join("stylesheets/admin.css")).to_i
7
8 assert_equal "#{path}?v=#{expected_mtime}", mtime_busted_path(path)
9 end
10
11 test "mtime_busted_path raises clearly for a missing file rather than silently omitting the version" do
12 assert_raises(RuntimeError) { mtime_busted_path("/stylesheets/does_not_exist.css") }
13 end
4end 14end
diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb
index 2da82d7..a7ed478 100644
--- a/test/models/helpers/content_helper_test.rb
+++ b/test/models/helpers/content_helper_test.rb
@@ -1,4 +1,8 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class ContentHelperTest < ActionView::TestCase 3class ContentHelperTest < ActionView::TestCase
4 test "weekday_abbr delegates through the current I18n locale" do
5 I18n.locale = :de
6 assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06"))
7 end
4end 8end
diff --git a/test/models/helpers/datetime_helper_test.rb b/test/models/helpers/datetime_helper_test.rb
new file mode 100644
index 0000000..a24e5d3
--- /dev/null
+++ b/test/models/helpers/datetime_helper_test.rb
@@ -0,0 +1,19 @@
1require 'test_helper'
2
3class DatetimeHelperTest < ActionView::TestCase
4 test "relative_time_phrase pluralizes correctly in English and German" do
5 travel_to Time.zone.parse("2026-07-12 12:00:00") do
6 I18n.with_locale(:en) do
7 assert_equal "1 day ago", relative_time_phrase(1.day.ago)
8 assert_equal "3 days ago", relative_time_phrase(3.days.ago)
9 end
10 I18n.with_locale(:de) do
11 assert_equal "vor 1 Tag", relative_time_phrase(1.day.ago)
12 assert_equal "vor 3 Tagen", relative_time_phrase(3.days.ago)
13 assert_equal "vor 1 Monat", relative_time_phrase(30.days.ago)
14 assert_equal "vor 2 Monaten", relative_time_phrase(45.days.ago)
15 assert_equal "vor 2 Jahren", relative_time_phrase(2.years.ago)
16 end
17 end
18 end
19end
diff --git a/test/models/helpers/events_helper_test.rb b/test/models/helpers/events_helper_test.rb
index 2e7567e..0486b3e 100644
--- a/test/models/helpers/events_helper_test.rb
+++ b/test/models/helpers/events_helper_test.rb
@@ -1,4 +1,25 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class EventsHelperTest < ActionView::TestCase 3class EventsHelperTest < ActionView::TestCase
4 test "rrule_with_break_opportunities inserts a break opportunity after each semicolon" do
5 result = rrule_with_break_opportunities("FREQ=MONTHLY;BYMONTH=1,2,3;BYDAY=-1TH")
6 assert_equal "FREQ=MONTHLY;<wbr>BYMONTH=1,2,3;<wbr>BYDAY=-1TH", result
7 assert result.html_safe?
8 end
9
10 test "rrule_with_break_opportunities escapes HTML-significant characters" do
11 result = rrule_with_break_opportunities("FREQ=WEEKLY;BYDAY=<script>")
12 assert_no_match /<script>/, result
13 assert_match "&lt;script&gt;", result
14 end
15
16 test "rrule_with_break_opportunities returns an empty string for blank input" do
17 assert_equal "", rrule_with_break_opportunities(nil)
18 assert_equal "", rrule_with_break_opportunities("")
19 end
20
21 test "rrule_with_break_opportunities preserves a trailing semicolon" do
22 result = rrule_with_break_opportunities("FREQ=WEEKLY;")
23 assert_equal "FREQ=WEEKLY;<wbr>", result
24 end
4end 25end
diff --git a/test/models/helpers/nodes_helper_test.rb b/test/models/helpers/nodes_helper_test.rb
index 13011de..5ab924f 100644
--- a/test/models/helpers/nodes_helper_test.rb
+++ b/test/models/helpers/nodes_helper_test.rb
@@ -1,4 +1,36 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class NodesHelperTest < ActionView::TestCase 3class NodesHelperTest < ActionView::TestCase
4 FakePage = Struct.new(:tag_list)
5
6 test "default_event_tag_mapping matches erfa-detail" do
7 page = FakePage.new(["erfa-detail"])
8 assert_equal ["erfa-detail", "open-day"], default_event_tag_mapping(page)
9 end
10
11 test "default_event_tag_mapping matches chaostreff-detail" do
12 page = FakePage.new(["chaostreff-detail"])
13 assert_equal ["chaostreff-detail", "open-day"], default_event_tag_mapping(page)
14 end
15
16 test "default_event_tag_mapping returns nil for unrelated tags" do
17 page = FakePage.new(["update"])
18 assert_nil default_event_tag_mapping(page)
19 end
20
21 test "default_event_tag_list is nil without a matching tag" do
22 page = FakePage.new([])
23 assert_nil default_event_tag_list(page)
24 end
25
26 test "sitemap_node_open? is false for a configured collapsed path" do
27 club = Node.root.children.create!(:slug => "club")
28 erfas = club.children.create!(:slug => "erfas")
29 assert_equal false, sitemap_node_open?(erfas)
30 end
31
32 test "sitemap_node_open? is true for anything not configured as collapsed" do
33 node = Node.root.children.create!(:slug => "sitemap_open_test")
34 assert_equal true, sitemap_node_open?(node)
35 end
4end 36end
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 514ba3f..91a4a6d 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -98,6 +98,7 @@ class NodeTest < ActiveSupport::TestCase
98 assert_not_nil node.draft 98 assert_not_nil node.draft
99 assert_nil node.draft.user 99 assert_nil node.draft.user
100 assert_nil node.head 100 assert_nil node.head
101 assert_nil node.autosave
101 end 102 end
102 103
103 def test_create_new_draft_of_published_page 104 def test_create_new_draft_of_published_page
@@ -110,15 +111,19 @@ class NodeTest < ActiveSupport::TestCase
110 node.publish_draft! 111 node.publish_draft!
111 assert_not_nil node.find_or_create_draft( @user1 ) 112 assert_not_nil node.find_or_create_draft( @user1 )
112 end 113 end
113 114
114 def test_find_or_create_draft_if_draft_exists_and_is_owned_by_user 115 def test_find_or_create_draft_if_draft_exists_and_is_owned_by_user
115 node = Node.root.children.create :slug => "xyz" 116 node = Node.root.children.create :slug => "xyz"
116 node.publish_draft! 117 node.publish_draft!
117 118
118 node.find_or_create_draft @user1 119 first_call = node.find_or_create_draft @user1
119 node.find_or_create_draft @user1 120 second_call = node.find_or_create_draft @user1
121
122 assert_equal first_call, second_call
123 assert_equal 2, node.pages.count
124 assert_equal @user1, node.lock_owner
120 end 125 end
121 126
122 def test_exception_if_draft_exists_but_locked_by_another_user 127 def test_exception_if_draft_exists_but_locked_by_another_user
123 node = Node.root.children.create :slug => "xyz" 128 node = Node.root.children.create :slug => "xyz"
124 node.publish_draft! 129 node.publish_draft!
@@ -280,6 +285,191 @@ class NodeTest < ActiveSupport::TestCase
280 node = Node.root.children.create( :slug => "wow" ) 285 node = Node.root.children.create( :slug => "wow" )
281 assert_nil node.draft.published_at 286 assert_nil node.draft.published_at
282 end 287 end
288
289 test "lock_for_editing! acquires the lock without creating a draft or autosave" do
290 node = create_node_with_published_page
291
292 node.lock_for_editing!(@user1)
293
294 assert_equal @user1, node.lock_owner
295 assert_nil node.draft
296 assert_nil node.autosave
297 end
298
299 test "autosave! creates a buffer that never appears among a node's pages, leaving the draft untouched" do
300 node = create_node_with_draft
301 node.lock_for_editing!(@user1)
302 page_count_before = node.pages.count
303
304 node.autosave!({ :title => "in progress" }, @user1)
305 node.reload
306
307 assert_not_nil node.autosave
308 assert_nil node.autosave.node_id
309 assert_equal page_count_before, node.pages.count
310 assert_not_equal "in progress", node.draft.title
311 end
312
313 test "save_draft! promotes an autosave into an existing draft without creating a new revision" do
314 node = create_node_with_draft
315 node.lock_for_editing!(@user1)
316 node.autosave!({ :title => "in progress" }, @user1)
317 page_count_before = node.pages.count
318
319 node.save_draft!(@user1)
320 node.reload
321
322 assert_nil node.autosave
323 assert_equal "in progress", node.draft.title
324 assert_equal page_count_before, node.pages.count
325 end
326
327 test "save_draft! promotes an autosave into a brand new, correctly-revisioned draft when none exists" do
328 node = create_node_with_published_page
329 head_revision = node.head.revision
330
331 node.lock_for_editing!(@user1)
332 node.autosave!({ :title => "updated version" }, @user1)
333 node.reload
334
335 assert_nil node.draft
336 assert_nil node.autosave.node_id
337
338 node.save_draft!(@user1)
339 node.reload
340
341 assert_not_nil node.draft
342 assert_equal head_revision + 1, node.draft.revision
343 assert_equal head_revision, node.head.revision
344 assert_nil node.autosave
345 assert_equal 2, node.pages.count
346 assert_equal node.head.user, node.draft.user
347 assert_equal @user1, node.draft.editor
348 assert_equal node.head.published_at, node.draft.published_at
349 end
350
351 test "autosave!, save_draft!, and lock_for_editing! raise LockedByAnotherUser for a second user" do
352 node = create_node_with_published_page
353 node.lock_for_editing!(@user1)
354
355 assert_raise(LockedByAnotherUser) { node.autosave!({ :title => "x" }, @user2) }
356 assert_raise(LockedByAnotherUser) { node.save_draft!(@user2) }
357 assert_raise(LockedByAnotherUser) { node.lock_for_editing!(@user2) }
358
359 assert_equal @user1, node.reload.lock_owner
360 end
361
362 test "wipe_draft! leaves a fresh autosave and its lock untouched" do
363 node = create_node_with_published_page
364 node.lock_for_editing!(@user1)
365 node.autosave!({ :title => "still typing" }, @user1)
366
367 node.wipe_draft!
368 node.reload
369
370 assert_not_nil node.autosave
371 assert_equal @user1, node.lock_owner
372 end
373
374 test "wipe_draft! destroys a stale, orphaned autosave and releases its lock" do
375 node = create_node_with_published_page
376 node.lock_for_editing!(@user1)
377 node.autosave!({ :title => "abandoned mid-session" }, @user1)
378 node.autosave.update_column(:updated_at, 2.days.ago)
379
380 node.wipe_draft!
381 node.reload
382
383 assert_nil node.autosave
384 assert_nil node.lock_owner
385 end
386
387 test "revert! is a safe no-op on a fresh node with only a draft" do
388 node = create_node_with_draft
389 node.lock_for_editing!(@user1)
390
391 node.revert!(@user1)
392 node.reload
393
394 assert_not_nil node.draft
395 assert_equal @user1, node.lock_owner
396 end
397
398 test "revert! discards an autosave on a fresh node without touching its only draft" do
399 node = create_node_with_draft
400 node.lock_for_editing!(@user1)
401 node.autosave!({ :title => "typing" }, @user1)
402
403 node.revert!(@user1)
404 node.reload
405
406 assert_nil node.autosave
407 assert_not_nil node.draft
408 assert_equal @user1, node.lock_owner
409 end
410
411 test "revert! does nothing when a published node has no draft or autosave" do
412 node = create_node_with_published_page
413 node.lock_for_editing!(@user1)
414
415 node.revert!(@user1)
416 node.reload
417
418 assert_not_nil node.head
419 assert_nil node.draft
420 end
421
422 test "revert! discards a fresh autosave and releases the lock when no draft exists" do
423 node = create_node_with_published_page
424 node.lock_for_editing!(@user1)
425 node.autosave!({ :title => "in progress" }, @user1)
426
427 node.revert!(@user1)
428 node.reload
429
430 assert_nil node.autosave
431 assert_nil node.draft
432 assert_nil node.lock_owner
433 end
434
435 test "revert! destroys an existing draft and releases the lock" do
436 node = create_node_with_published_page
437 head_title = node.head.title
438 node.lock_for_editing!(@user1)
439 node.autosave!({ :title => "second version" }, @user1)
440 node.save_draft!(@user1)
441
442 node.revert!(@user1)
443 node.reload
444
445 assert_nil node.draft
446 assert_equal head_title, node.head.title
447 assert_nil node.lock_owner
448 end
449
450 test "revert! discards only the autosave when a draft survives beneath it" do
451 node = create_node_with_published_page
452 node.lock_for_editing!(@user1)
453 node.autosave!({ :title => "second version" }, @user1)
454 node.save_draft!(@user1)
455 node.autosave!({ :title => "third version, still typing" }, @user1)
456
457 node.revert!(@user1)
458 node.reload
459
460 assert_nil node.autosave
461 assert_not_nil node.draft
462 assert_equal "second version", node.draft.title
463 assert_equal @user1, node.lock_owner
464 end
465
466 test "revert! raises LockedByAnotherUser for a non-owner" do
467 node = create_node_with_published_page
468 node.lock_for_editing!(@user1)
469
470 assert_raise(LockedByAnotherUser) { node.revert!(@user2) }
471 assert_equal @user1, node.reload.lock_owner
472 end
283 473
284 def create_revisions node, count 474 def create_revisions node, count
285 count.times do 475 count.times do
@@ -287,4 +477,173 @@ class NodeTest < ActiveSupport::TestCase
287 node.publish_draft! 477 node.publish_draft!
288 end 478 end
289 end 479 end
480
481 test "available_layer_pairs matches the six-state table" do
482 node = Node.root.children.create!(:slug => "layer_pairs_test")
483 user = @user1 || User.find_by_login("aaron")
484
485 assert_equal [[:draft, :autosave]], (node.lock_for_editing!(user); node.autosave!({title: "v1"}, user); node.available_layer_pairs) # state F
486
487 node.save_draft!(user)
488 node.publish_draft!
489 assert_equal [], node.available_layer_pairs # state A
490
491 node.lock_for_editing!(user)
492 node.autosave!({title: "v2"}, user)
493 assert_equal [[:head, :autosave]], node.available_layer_pairs # state B
494
495 node.save_draft!(user)
496 assert_equal [[:head, :draft]], node.available_layer_pairs # state C
497
498 node.lock_for_editing!(user)
499 node.autosave!({title: "v3"}, user)
500 assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D
501 end
502
503 test "publishing a staged move under one's own descendant is rejected, not allowed to crash" do
504 a = Node.root.children.create!(:slug => "cycle_guard_a")
505 b = a.children.create!(:slug => "cycle_guard_b")
506
507 a.staged_parent_id = b.id
508
509 assert_raises(ActiveRecord::RecordInvalid) { a.publish_draft! }
510
511 a.reload
512 assert_equal Node.root.id, a.parent_id
513 end
514
515 test "editor_search matches a partial substring, not just a whole word" do
516 node = Node.root.children.create!(:slug => "editor_search_substring_test")
517 node.find_or_create_draft(@user1)
518 node.draft.update(:title => "Biometrics Conference")
519 node.publish_draft!
520
521 assert_includes Node.editor_search("bio"), node
522 assert_includes Node.editor_search("Conf"), node
523 end
524
525 test "editor_search returns an empty relation for a blank term, not every node" do
526 assert_equal 0, Node.editor_search("").count
527 assert_equal 0, Node.editor_search(nil).count
528 assert_equal 0, Node.editor_search(" ").count
529 end
530
531 test "editor_search requires every word to match, but each word can match a different field" do
532 node = Node.root.children.create!(:slug => "editor_search_multiword_test")
533 node.find_or_create_draft(@user1)
534 node.draft.update(:title => "Backspace e.V. Bamberg", :abstract => "Spiegelgraben 41, 96052 Bamberg")
535 node.publish_draft!
536
537 assert_includes Node.editor_search("Backspace Spiegelgraben"), node
538 assert_equal 0, Node.editor_search("Backspace Nonexistentstreet").count
539 end
540
541 test "drafts_and_autosaves without a user sorts by recency only" do
542 older = Node.root.children.create!(:slug => "drafts_order_older")
543 older.find_or_create_draft(@user1)
544 newer = Node.root.children.create!(:slug => "drafts_order_newer")
545 newer.find_or_create_draft(@user1)
546
547 result = Node.drafts_and_autosaves.to_a
548 assert result.index(newer) < result.index(older)
549 end
550
551 test "drafts_and_autosaves with a user puts their own locked nodes first, regardless of recency" do
552 mine = Node.root.children.create!(:slug => "drafts_order_mine")
553 mine.lock_for_editing!(@user1)
554 mine.autosave!({:title => "mine"}, @user1)
555
556 someone_elses_newer = Node.root.children.create!(:slug => "drafts_order_theirs")
557 other_user = User.find_by_login("quentin")
558 someone_elses_newer.lock_for_editing!(other_user)
559 someone_elses_newer.autosave!({:title => "theirs"}, other_user)
560
561 result = Node.drafts_and_autosaves(current_user_id: @user1.id).to_a
562 assert result.index(mine) < result.index(someone_elses_newer)
563 end
564
565 test "recently_changed includes a node whose head was recently published" do
566 node = Node.root.children.create!(:slug => "recent_changed_published")
567 node.find_or_create_draft(@user1)
568 node.autosave!({:title => "v1"}, @user1)
569 node.save_draft!(@user1)
570 node.publish_draft!
571
572 assert_includes Node.recently_changed, node
573 end
574
575 test "recently_changed excludes a node only touched by locking or unlocking after an old publish" do
576 node = Node.root.children.create!(:slug => "recent_changed_lock_only")
577 node.find_or_create_draft(@user1)
578 node.autosave!({:title => "v1"}, @user1)
579 node.save_draft!(@user1)
580 node.publish_draft!
581 node.head.update_column(:updated_at, 20.days.ago)
582
583 node.lock_for_editing!(@user1)
584 node.unlock!
585
586 assert_not_includes Node.recently_changed, node
587 end
588
589 test "autosave! carries over the current related assets to the newly created autosave row" do
590 node = Node.root.children.create!(:slug => "autosave_asset_carryover_test")
591 user = User.find_by_login("quentin")
592 asset = Asset.create!(:name => "carryover-photo", :upload_content_type => "image/png")
593 node.draft.assets << asset
594
595 node.lock_for_editing!(user)
596 node.autosave!({:title => "v1"}, user)
597
598 assert_includes node.autosave.reload.assets, asset
599 end
600
601 test "autosave! does not reset assets already attached directly to an existing autosave" do
602 node = Node.root.children.create!(:slug => "autosave_asset_no_reset_test")
603 user = User.find_by_login("quentin")
604 original = Asset.create!(:name => "original-photo", :upload_content_type => "image/png")
605 extra = Asset.create!(:name => "extra-photo", :upload_content_type => "image/png")
606 node.draft.assets << original
607
608 node.lock_for_editing!(user)
609 node.autosave!({:title => "v1"}, user)
610 node.autosave.assets << extra
611
612 node.autosave!({:title => "v2"}, user)
613
614 assert_includes node.autosave.reload.assets, original
615 assert_includes node.autosave.reload.assets, extra
616 end
617
618 test "autosave! carries over other-locale translations to the newly created autosave row" do
619 node = Node.root.children.create!(:slug => "autosave_translation_carryover_test")
620 user = User.find_by_login("quentin")
621
622 Globalize.with_locale(:en) { node.draft.update!(:title => "English title") }
623
624 node.lock_for_editing!(user)
625 Globalize.with_locale(:de) { node.autosave!({:title => "German edit"}, user) }
626
627 autosave = node.autosave.reload
628 assert_includes autosave.translated_locales, :en
629 assert_equal "English title", Globalize.with_locale(:en) { autosave.title }
630 assert_equal "German edit", Globalize.with_locale(:de) { autosave.title }
631 end
632
633 test "autosave! does not reset other-locale translations already attached directly to an existing autosave" do
634 node = Node.root.children.create!(:slug => "autosave_translation_no_reset_test")
635 user = User.find_by_login("quentin")
636
637 Globalize.with_locale(:en) { node.draft.update!(:title => "Original English title") }
638
639 node.lock_for_editing!(user)
640 Globalize.with_locale(:de) { node.autosave!({:title => "v1"}, user) }
641 Globalize.with_locale(:en) { node.autosave.update!(:title => "Edited directly on autosave") }
642
643 Globalize.with_locale(:de) { node.autosave!({:title => "v2"}, user) }
644
645 autosave = node.autosave.reload
646 assert_equal "Edited directly on autosave", Globalize.with_locale(:en) { autosave.title }
647 assert_equal "v2", Globalize.with_locale(:de) { autosave.title }
648 end
290end 649end
diff --git a/test/models/page_test.rb b/test/models/page_test.rb
index afba8b5..24bc7c5 100644
--- a/test/models/page_test.rb
+++ b/test/models/page_test.rb
@@ -142,5 +142,232 @@ class PageTest < ActiveSupport::TestCase
142 update = updates2009.children.create!( :slug => "my-first-update" ) 142 update = updates2009.children.create!( :slug => "my-first-update" )
143 assert_equal "update", update.draft.template_name 143 assert_equal "update", update.draft.template_name
144 end 144 end
145 145
146 test "a page scheduled for future publication is not yet public even after being published" do
147 node = Node.root.children.create!(slug: "preview-scheduled-test")
148 draft = node.find_or_create_draft(@user1)
149 draft.title = "Scheduled test"
150 draft.published_at = 1.day.from_now
151 draft.save!
152 token = draft.ensure_preview_token!
153
154 node.publish_draft!
155 page = Page.find_by(preview_token: token)
156
157 assert_equal page.id, page.node.head_id
158 assert_not page.public?
159 end
160
161 test "a superseded page is no longer the head, even though it was once published" do
162 node = Node.root.children.create!(slug: "preview-superseded-test")
163 first_draft = node.find_or_create_draft(@user1)
164 first_draft.title = "First version"
165 first_draft.save!
166 first_token = first_draft.ensure_preview_token!
167 node.publish_draft!
168
169 second_draft = node.find_or_create_draft(@user1)
170 second_draft.title = "Second version"
171 second_draft.save!
172 node.publish_draft!
173
174 first_page = Page.find_by(preview_token: first_token)
175
176 assert_not_equal first_page.id, first_page.node.head_id
177 assert first_page.published_at.present?
178 end
179
180 test "clone_attributes_from preserves an unchanged locale's original timestamp" do
181 n = Node.root.children.create!(:slug => "clone_preserve_timestamp_test")
182 source = n.draft
183 Globalize.with_locale(:de) { source.update!(:title => "Deutscher Titel") }
184 Globalize.with_locale(:en) { source.update!(:title => "English Title") }
185
186 target = Page.create!
187 target.clone_attributes_from(source)
188 original_en_updated_at = target.translations.find_by(:locale => :en).updated_at
189
190 Globalize.with_locale(:de) { source.update!(:title => "Deutscher Titel (bearbeitet)") }
191 target.clone_attributes_from(source)
192
193 en_translation = target.translations.find_by(:locale => :en)
194 assert_equal "English Title", en_translation.title
195 assert_equal original_en_updated_at, en_translation.updated_at
196 end
197
198 test "clone_attributes_from gives a genuinely changed locale a fresh timestamp" do
199 n = Node.root.children.create!(:slug => "clone_fresh_timestamp_test")
200 source = n.draft
201 Globalize.with_locale(:de) { source.update!(:title => "Erste Version") }
202
203 target = Page.create!
204 target.clone_attributes_from(source)
205 original_de_updated_at = target.translations.find_by(:locale => :de).updated_at
206
207 Globalize.with_locale(:de) { source.update!(:title => "Zweite Version") }
208 target.clone_attributes_from(source)
209
210 de_translation = target.translations.find_by(:locale => :de)
211 assert_equal "Zweite Version", de_translation.title
212 assert_operator de_translation.updated_at, :>, original_de_updated_at
213 end
214
215 test "clone_attributes_from removes a locale no longer present in the source" do
216 n = Node.root.children.create!(:slug => "clone_removed_locale_test")
217 source = n.draft
218 Globalize.with_locale(:en) { source.update!(:title => "English Title") }
219
220 target = Page.create!
221 target.clone_attributes_from(source)
222 assert_includes target.translations.map(&:locale), :en
223
224 source.translations.where(:locale => :en).delete_all
225 target.clone_attributes_from(source)
226
227 assert_not_includes target.reload.translations.map(&:locale), :en
228 end
229
230 def test_diff_against_inline_keeps_tags_and_marks_only_the_changed_word
231 n = Node.root.children.create! :slug => "diff_against_test"
232 d = n.find_or_create_draft @user1
233 d.title = "Old heading"
234 d.save!
235 n.publish_draft!
236
237 d2 = n.find_or_create_draft @user1
238 d2.title = "New heading"
239 d2.save!
240
241 diff = d2.diff_against(n.head)
242
243 assert_match "<del>Old</del>", diff[:title]
244 assert_match "<ins>New</ins>", diff[:title]
245 end
246
247 def test_diff_against_side_by_side_returns_two_annotated_strings
248 n = Node.root.children.create! :slug => "diff_against_sbs_test"
249 d = n.find_or_create_draft @user1
250 d.title = "Old heading"
251 d.save!
252 n.publish_draft!
253
254 d2 = n.find_or_create_draft @user1
255 d2.title = "New heading"
256 d2.save!
257
258 old_html, new_html = d2.diff_against(n.head, view: :side_by_side)[:title]
259
260 assert_match "<del>Old</del>", old_html
261 assert_match "<ins>New</ins>", new_html
262 end
263
264 test "diff_against handles an inserted paragraph split without corrupting the document" do
265 n = Node.root.children.create! :slug => "paragraph_split_test"
266 d = n.find_or_create_draft @user1
267 d.body = "<p>Der Vortragsraum ist ab 19 Uhr geöffnet, der Zugang erfolgt über den Hinterhof.</p>"
268 d.save!
269 n.publish_draft!
270
271 d2 = n.find_or_create_draft @user1
272 d2.body = "<p>Der Vortragsraum ist ab 19 Uhr geöffnet,</p>\n<p>der Zugang erfolgt über den Hinterhof.</p>"
273 d2.save!
274
275 diff = d2.diff_against(n.head)
276 fragment = Nokogiri::HTML::DocumentFragment.parse(diff[:body])
277
278 assert_equal 2, fragment.css('ins.diff_structural').length
279 assert_match "der Zugang erfolgt über den Hinterhof.", fragment.text
280 end
281
282 test "diff_against reports tag and template changes" do
283 n = Node.root.children.create! :slug => "field_diff_test"
284 d = n.find_or_create_draft @user1
285 d.tag_list = "update"
286 d.template_name = "standard_template"
287 d.save!
288 n.publish_draft!
289
290 d2 = n.find_or_create_draft @user1
291 d2.tag_list = "update, pressemitteilung"
292 d2.template_name = "title_only"
293 d2.save!
294
295 diff = d2.diff_against(n.head)
296
297 assert_equal ["pressemitteilung"], diff[:tags][:added]
298 assert_equal [], diff[:tags][:removed]
299 assert diff[:template_name][:changed]
300 assert_equal "standard_template", diff[:template_name][:from]
301 assert_equal "title_only", diff[:template_name][:to]
302 end
303
304 test "diff_against reports added and removed assets by filename" do
305 n = Node.root.children.create! :slug => "asset_diff_test"
306 d = n.find_or_create_draft @user1
307 d.save!
308 n.publish_draft!
309
310 kept_asset = Asset.create!(:upload_file_name => "kept.png", :upload_content_type => "image/png", :upload_file_size => 1)
311 removed_asset = Asset.create!(:upload_file_name => "removed.pdf", :upload_content_type => "application/pdf", :upload_file_size => 1)
312 n.head.update_assets([kept_asset.id, removed_asset.id])
313
314 d2 = n.find_or_create_draft @user1
315 added_asset = Asset.create!(:upload_file_name => "added.png", :upload_content_type => "image/png", :upload_file_size => 1)
316 d2.update_assets([kept_asset.id, added_asset.id])
317 d2.save!
318
319 diff = d2.diff_against(n.head)
320
321 assert_equal [added_asset], diff[:assets][:added]
322 assert_equal [removed_asset], diff[:assets][:removed]
323 end
324
325 test "diff_against with an explicit locale compares that locale's own translation on each side" do
326 n = Node.root.children.create!(:slug => "diff_locale_test")
327 d = n.find_or_create_draft(@user1)
328 Globalize.with_locale(:en) { d.update!(:title => "Old English") }
329 d.save!
330 n.publish_draft!
331
332 d2 = n.find_or_create_draft(@user1)
333 Globalize.with_locale(:en) { d2.update!(:title => "New English") }
334 d2.save!
335
336 diff = d2.diff_against(n.head, :locale => :en)
337
338 assert_match "<del>Old</del>", diff[:title]
339 assert_match "<ins>New</ins>", diff[:title]
340 end
341
342 test "diff_against with an explicit locale ignores content in other locales entirely" do
343 n = Node.root.children.create!(:slug => "diff_locale_isolation_test")
344 d = n.find_or_create_draft(@user1)
345 d.save!
346 n.publish_draft!
347
348 d2 = n.find_or_create_draft(@user1)
349 Globalize.with_locale(:de) { d2.update!(:title => "Nur Deutsch geändert") }
350 d2.save!
351
352 diff = d2.diff_against(n.head, :locale => :en)
353
354 assert_no_match(/Deutsch/, diff[:title])
355 end
356
357 test "locale_diff_summary flags a locale that only exists on one side as changed" do
358 n = Node.root.children.create!(:slug => "diff_locale_summary_test")
359 d = n.find_or_create_draft(@user1)
360 d.save!
361 n.publish_draft!
362
363 d2 = n.find_or_create_draft(@user1)
364 Globalize.with_locale(:en) { d2.update!(:title => "New English translation") }
365 d2.save!
366
367 summary = d2.locale_diff_summary(n.head)
368 en_entry = summary.find { |s| s[:locale] == :en }
369
370 assert en_entry[:changed]
371 refute en_entry[:exists_there]
372 end
146end 373end