feat: grow blog_demo

This commit is contained in:
dualfroz committed 2026-09-01 16:54:16 +02:00
1 parent 4d4bf2f007
commit 30a1b4828a
1 file changed
+173
+173
View File
@@ -683,6 +683,179 @@ Net::HTTP.start('127.0.0.1', port) do |http|
noop_cookie = cookie_from(noop)
cookie = noop_cookie unless noop_cookie.empty?
noop_show = http.get(post_path, 'Cookie' => cookie)
unless noop_show.body.include?('Nothing changed') || noop_show.body.include?('Nic sie nie zmienilo')
raise 'submitting identical values should report that Prism::Orm::Dirty saw zero real changes'
end
puts "POST #{post_path} with identical values -> Post#saved_changes was empty, notice said nothing changed"
# Prism::Orm::Locking: two "browser tabs" open the same edit form (each
# capturing the lock_version the row had at that moment), tab A saves
# first and bumps it, then tab B - still holding the OLD lock_version in
# its hidden field - submits on top of that. Store#update's version check
# runs inside the same PStore transaction as the merge, so tab B's write
# cannot silently clobber tab A's: it must come back as a 409 with the
# conflict notice, and tab A's title must survive untouched.
tab_a_edit = http.get("#{post_path}/edit", 'Cookie' => cookie)
tab_b_edit = http.get("#{post_path}/edit", 'Cookie' => cookie)
tab_a_lock_version = tab_a_edit.body[/name="post\[lock_version\]"[^>]*value="([^"]*)"/, 1]
tab_b_lock_version = tab_b_edit.body[/name="post\[lock_version\]"[^>]*value="([^"]*)"/, 1]
raise 'edit form should carry the current lock_version in a hidden field' if tab_a_lock_version.to_s.empty?
raise 'both tabs should have observed the same lock_version before either saved' unless tab_a_lock_version == tab_b_lock_version
tab_a_save_body = URI.encode_www_form(
'post[title]' => 'Hello from Prism (tab A won)', 'post[body]' => 'Saved first.',
'post[lock_version]' => tab_a_lock_version, '_method' => 'PATCH', 'authenticity_token' => token
)
tab_a_save = http.post(post_path, tab_a_save_body, 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected tab A's save to succeed, got #{tab_a_save.code}" unless tab_a_save.code == '302'
tab_a_cookie = cookie_from(tab_a_save)
cookie = tab_a_cookie unless tab_a_cookie.empty?
puts "POST #{post_path}/edit tab A (lock_version=#{tab_a_lock_version}) -> #{tab_a_save.code}, saved first"
tab_b_save_body = URI.encode_www_form(
'post[title]' => 'Hello from Prism (tab B lost)', 'post[body]' => 'Submitted stale, should be rejected.',
'post[lock_version]' => tab_b_lock_version, '_method' => 'PATCH', 'authenticity_token' => token
)
tab_b_save = http.post(post_path, tab_b_save_body, 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
unless tab_b_save.code == '409'
raise "expected tab B's stale save to be rejected with 409, got #{tab_b_save.code}"
end
unless tab_b_save.body.include?('Someone else changed this post') || tab_b_save.body.include?('Ktos inny zmienil ten wpis')
raise 'the 409 response should render the stale-conflict notice'
end
puts "POST #{post_path}/edit tab B (stale lock_version=#{tab_b_lock_version}) -> #{tab_b_save.code} (expected 409, StaleObjectError caught)"
after_conflict = http.get(post_path, 'Cookie' => cookie)
raise "tab B's rejected write must not have overwritten tab A's title" unless after_conflict.body.include?('Hello from Prism (tab A won)')
raise "tab B's rejected write must not have leaked through" if after_conflict.body.include?('tab B lost')
puts "GET #{post_path} -> tab A's title survives untouched, Prism::Orm::Locking prevented the lost update"
puts '-' * 40
puts 'Post.accepts_nested_attributes_for :comments - the author moderates a comment from the POST edit form ' \
'itself, not that comment\'s own /comments/:id route'
moderation_comment_body = URI.encode_www_form(
'comment[author]' => 'Someone to moderate', 'comment[body]' => 'unmoderated text', 'authenticity_token' => token
)
moderation_comment = http.post("/posts/#{post_id}/comments", moderation_comment_body,
'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected the extra comment to redirect, got #{moderation_comment.code}" unless moderation_comment.code == '302'
moderation_comment_cookie = cookie_from(moderation_comment)
cookie = moderation_comment_cookie unless moderation_comment_cookie.empty?
before_moderation_home = http.get('/', 'Cookie' => cookie)
unless before_moderation_home.body.include?('2 comment(s)')
raise 'expected comments_count to read 2 before the moderation scene (the tab-A comment plus this new one)'
end
puts "POST /posts/#{post_id}/comments -> #{moderation_comment.code} (a second comment to moderate through the post form)"
moderation_edit_page = http.get("#{post_path}/edit", 'Cookie' => cookie)
moderation_index = nil
moderation_edit_page.body.scan(/name="post\[comments_attributes\]\[(\d+)\]\[id\]"[^>]*value="(\d+)"/) do |index, id|
moderation_index = index if id.to_i != comment_id.to_i
end
raise 'edit form should render a nested field for the comment to be moderated' unless moderation_index
raise 'moderation nested field should carry the unmoderated body' unless moderation_edit_page.body.include?('unmoderated text')
puts "GET #{post_path}/edit -> FormBuilder#fields_for rendered comments_attributes[#{moderation_index}] for the new comment"
moderation_lock_version = moderation_edit_page.body[/name="post\[lock_version\]"[^>]*value="([^"]*)"/, 1]
moderate_body_edit = URI.encode_www_form(
'post[title]' => 'Hello from Prism (tab A won)', 'post[body]' => 'Saved first.',
'post[lock_version]' => moderation_lock_version,
"post[comments_attributes][#{moderation_index}][id]" => moderation_edit_page.body[/name="post\[comments_attributes\]\[#{moderation_index}\]\[id\]"[^>]*value="(\d+)"/, 1],
"post[comments_attributes][#{moderation_index}][body]" => 'Edited by the post author, not the commenter',
'_method' => 'PATCH', 'authenticity_token' => token
)
moderate_edit = http.post(post_path, moderate_body_edit, 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected the moderation edit to redirect, got #{moderate_edit.code}" unless moderate_edit.code == '302'
moderate_edit_cookie = cookie_from(moderate_edit)
cookie = moderate_edit_cookie unless moderate_edit_cookie.empty?
puts "POST #{post_path} with comments_attributes[#{moderation_index}][body] -> #{moderate_edit.code}"
moderated_show = http.get(post_path, 'Cookie' => cookie)
unless moderated_show.body.include?('Edited by the post author, not the commenter')
raise 'expected the comment moderated through the post edit form to show its new body on the post page'
end
raise 'the other comment must not have been touched by this moderation edit' unless moderated_show.body.include?('Nice post, tab A won!')
puts "GET #{post_path} -> the moderated comment shows its new body, the untouched comment (tab A/B locking scene) is unaffected"
moderation_edit_page_2 = http.get("#{post_path}/edit", 'Cookie' => cookie)
moderation_lock_version_2 = moderation_edit_page_2.body[/name="post\[lock_version\]"[^>]*value="([^"]*)"/, 1]
destroy_via_moderation_body = URI.encode_www_form(
'post[title]' => 'Hello from Prism (tab A won)', 'post[body]' => 'Saved first.',
'post[lock_version]' => moderation_lock_version_2,
"post[comments_attributes][#{moderation_index}][id]" => moderation_edit_page.body[/name="post\[comments_attributes\]\[#{moderation_index}\]\[id\]"[^>]*value="(\d+)"/, 1],
"post[comments_attributes][#{moderation_index}][_destroy]" => '1',
'_method' => 'PATCH', 'authenticity_token' => token
)
destroy_via_moderation = http.post(post_path, destroy_via_moderation_body,
'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected the moderation destroy to redirect, got #{destroy_via_moderation.code}" unless destroy_via_moderation.code == '302'
moderation_destroy_cookie = cookie_from(destroy_via_moderation)
cookie = moderation_destroy_cookie unless moderation_destroy_cookie.empty?
puts "POST #{post_path} with comments_attributes[#{moderation_index}][_destroy]=1 -> #{destroy_via_moderation.code} " \
'(comment removed through accepts_nested_attributes_for, not its own /comments/:id route)'
after_moderation_destroy = http.get('/', 'Cookie' => cookie)
unless after_moderation_destroy.body.include?('1 comment(s)')
raise 'expected comments_count to drop back to 1 - counter_cache after_destroy still fires when a comment ' \
'is destroyed through nested attributes, not just through CommentsController#destroy'
end
puts "GET / -> post listing shows '1 comment(s)' again (counter_cache after_destroy ran for the nested-attributes destroy too)"
puts '-' * 40
puts 'CommentsController#index - comments now get their own paginated URL, not just whatever fits ' \
'inline on posts#show'
11.times do |i|
filler_comment_body = URI.encode_www_form(
'comment[author]' => 'Pager', 'comment[body]' => "Filler comment #{i + 1}", 'authenticity_token' => token
)
filler_comment = http.post("/posts/#{post_id}/comments", filler_comment_body,
'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected filler comment #{i + 1} to redirect, got #{filler_comment.code}" unless filler_comment.code == '302'
end
puts 'created 11 filler comments (12 total: the surviving tab-A comment plus these) to exercise ' \
'Orm::Pagination on comments#index'
comments_page_one = http.get("/posts/#{post_id}/comments", 'Cookie' => cookie)
comments_page_two = http.get("/posts/#{post_id}/comments?page=2", 'Cookie' => cookie)
puts "GET /posts/#{post_id}/comments -> #{comments_page_one.code}, GET ...?page=2 -> #{comments_page_two.code}"
raise 'comments page 1 should render pagination nav links' unless comments_page_one.body.include?('class="pagination"')
unless comments_page_one.body.scan('class="comment"').size == 10
raise 'comments page 1 should show exactly 10 comments (Orm::Pagination default per-page)'
end
raise 'comments page 2 should show the remaining 2 comments' unless comments_page_two.body.scan('class="comment"').size == 2
comments_etag = comments_page_one['etag']
raise 'comments#index should set an ETag now that it uses fresh_when' unless comments_etag
puts "GET /posts/#{post_id}/comments -> ETag=#{comments_etag}"
conditional_comments = http.get("/posts/#{post_id}/comments", 'Cookie' => cookie, 'If-None-Match' => comments_etag)
puts "GET /posts/#{post_id}/comments with If-None-Match: #{comments_etag} -> #{conditional_comments.code}"
unless conditional_comments.code == '304'
raise "expected a fresh conditional GET on comments#index to return 304, got #{conditional_comments.code}"
end
raise '304 response on comments#index must not carry a body' unless conditional_comments.body.to_s.empty?
one_more_comment_body = URI.encode_www_form(
'comment[author]' => 'Pager', 'comment[body]' => 'One more filler comment', 'authenticity_token' => token
)
one_more_comment = http.post("/posts/#{post_id}/comments", one_more_comment_body,
'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
raise "expected the extra filler comment to redirect, got #{one_more_comment.code}" unless one_more_comment.code == '302'
now_stale_comments = http.get("/posts/#{post_id}/comments", 'Cookie' => cookie, 'If-None-Match' => comments_etag)
puts "GET /posts/#{post_id}/comments with the OLD If-None-Match after a new comment -> #{now_stale_comments.code}"
unless now_stale_comments.code == '200'
raise "expected the stale conditional GET on comments#index to return 200, got #{now_stale_comments.code}"
end
raise 'the fresh comments#index render must carry a NEW ETag, not the stale one' if now_stale_comments['etag'] == comments_etag
comments_json = http.get("/posts/#{post_id}/comments.json", 'Cookie' => cookie)
parsed_comments_json = JSON.parse(comments_json.body)
puts "GET /posts/#{post_id}/comments.json -> #{comments_json.code} (#{parsed_comments_json.size} comments on this page)"
raise 'comments#index.json should return only the current page (10), not every comment' unless parsed_comments_json.size == 10
unless parsed_comments_json.first.keys.sort == %w[author body id post_id status]
raise 'comments#index.json entries should carry exactly id/author/body/post_id/status'
end
server.stop