feat: grow blog_demo
This commit is contained in:
1 parent
4d4bf2f007
commit
30a1b4828a
1 file changed
+173
+173
@@ -683,6 +683,179 @@ Net::HTTP.start('127.0.0.1', port) do |http|
|
|||||||
noop_cookie = cookie_from(noop)
|
noop_cookie = cookie_from(noop)
|
||||||
cookie = noop_cookie unless noop_cookie.empty?
|
cookie = noop_cookie unless noop_cookie.empty?
|
||||||
noop_show = http.get(post_path, 'Cookie' => cookie)
|
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
|
end
|
||||||
|
|
||||||
server.stop
|
server.stop
|
||||||
|
|||||||
Reference in new issue
Block a user