From 35141a84d55aa42c89ee5aba5027714d91be5d27 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 13:37:02 +0200 Subject: [PATCH] feat: extend blog_demo --- blog_demo.rb | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 3 deletions(-) diff --git a/blog_demo.rb b/blog_demo.rb index a56f24a..ebda04b 100644 --- a/blog_demo.rb +++ b/blog_demo.rb @@ -164,9 +164,179 @@ Net::HTTP.start('127.0.0.1', port) do |http| post_create_cookie = cookie_from(create) cookie = post_create_cookie unless post_create_cookie.empty? - listing = http.get('/posts') - puts "GET /posts -> #{listing.code}" - puts listing.body + show = http.get(post_path, 'Cookie' => cookie) + puts "GET #{post_path} -> #{show.code}" + raise 'cover filename missing from show page' unless show.body.include?('/uploads/') + post_id = post_path.split('/').last + # posts#show is slug-keyed now (see app.rb's `param: :slug`), so post_id + # above is really the post's SLUG - fine for building URLs (that's all + # it's used for almost everywhere below), but Like.likeable_id is a real + # numeric FK (see LikesController#find_likeable!, which does + # klass.find(params['likeable_id'])), so the handful of places that POST + # to /likes or query the Like table directly need this actual row id + # instead, resolved once here since it never changes across the post's + # own trash/restore cycle later in this script. + post_row_id = Post.find_by!(slug: post_id).id + cover_path = show.body[%r{/uploads/([^"']+)}, 0] + cover_file = File.join(__dir__, 'examples/blog/public', cover_path) + raise "uploaded cover file missing on disk: #{cover_file}" unless File.exist?(cover_file) + + puts '-' * 40 + puts 'HTTP Range requests against the uploaded cover (Prism::HttpRange, RFC 7233), served by ' \ + 'Middleware::Static over the real Prism::Server socket - not just .call(env) in a unit test' + cover_size = File.size(cover_file) + full = http.get(cover_path, 'Cookie' => cookie) + puts "GET #{cover_path} (no Range) -> #{full.code}, Accept-Ranges=#{full['accept-ranges']}, " \ + "Content-Length=#{full['content-length']}" + raise "expected 200 with the whole cover, got #{full.code}" unless full.code == '200' + raise 'expected Accept-Ranges: bytes on an ordinary 200' unless full['accept-ranges'] == 'bytes' + raise 'expected the full cover body on a plain GET' unless full.body.bytesize == cover_size + + prefix = http.get(cover_path, 'Cookie' => cookie, 'Range' => 'bytes=0-2') + puts "GET #{cover_path} (Range: bytes=0-2) -> #{prefix.code}, Content-Range=#{prefix['content-range']}, " \ + "body=#{prefix.body.inspect}" + raise "expected 206 for a satisfiable prefix range, got #{prefix.code}" unless prefix.code == '206' + raise "expected Content-Range bytes 0-2/#{cover_size}" unless prefix['content-range'] == "bytes 0-2/#{cover_size}" + raise "expected only the first 3 bytes on disk, got #{prefix.body.inspect}" unless prefix.body == 'not' + + suffix = http.get(cover_path, 'Cookie' => cookie, 'Range' => 'bytes=-5') + puts "GET #{cover_path} (Range: bytes=-5) -> #{suffix.code}, Content-Range=#{suffix['content-range']}, " \ + "body=#{suffix.body.inspect}" + raise "expected 206 for a satisfiable suffix range, got #{suffix.code}" unless suffix.code == '206' + raise "expected the last 5 bytes actually read off disk, got #{suffix.body.inspect}" unless suffix.body == 'bytes' + + unsatisfiable = http.get(cover_path, 'Cookie' => cookie, 'Range' => "bytes=#{cover_size + 100}-#{cover_size + 200}") + puts "GET #{cover_path} (Range: bytes=#{cover_size + 100}-#{cover_size + 200}, beyond EOF) -> " \ + "#{unsatisfiable.code}, Content-Range=#{unsatisfiable['content-range']}" + raise "expected 416 for a range starting past EOF, got #{unsatisfiable.code}" unless unsatisfiable.code == '416' + unless unsatisfiable['content-range'] == "bytes */#{cover_size}" + raise "expected Content-Range: bytes */#{cover_size} on 416, got #{unsatisfiable['content-range'].inspect}" + end + puts "cover (#{cover_size} bytes) served correctly whole, as a 3-byte prefix, a 5-byte suffix, and rejected " \ + 'with 416 for an out-of-range request - all through the real TCP socket, not a direct middleware call' + + puts '-' * 40 + puts 'Conditional GET against the SAME cover file (Middleware::Static hands out only a cheap WEAK ETag - ' \ + 'size+mtime, no fingerprint - unlike the CSS scene above), over the real socket' + cover_etag = full['etag'] + raise "expected a weak ETag (W/ prefix) on a plain static file, got #{cover_etag.inspect}" unless cover_etag&.start_with?('W/') + cover_not_modified = http.get(cover_path, 'Cookie' => cookie, 'If-None-Match' => cover_etag) + puts "GET #{cover_path} with If-None-Match: #{cover_etag} -> #{cover_not_modified.code}" + raise "expected 304 for a matching weak ETag, got #{cover_not_modified.code}" unless cover_not_modified.code == '304' + unless [nil, '0'].include?(cover_not_modified['content-length']) + raise "expected an empty (or absent) Content-Length on a 304, got #{cover_not_modified['content-length'].inspect}" + end + + cover_range_falls_back = http.get(cover_path, 'Cookie' => cookie, 'If-Range' => cover_etag, 'Range' => 'bytes=0-2') + puts "GET #{cover_path} with If-Range: #{cover_etag} (weak) + Range: bytes=0-2 -> #{cover_range_falls_back.code} " \ + '(RFC 7232 SS2.3.2: If-Range never honors a weak validator, so this must fall back to the full body)' + unless cover_range_falls_back.code == '200' + raise "expected 200 (Range ignored): a weak ETag can never satisfy If-Range, got #{cover_range_falls_back.code}" + end + raise 'expected the FULL cover body once If-Range falls back' unless cover_range_falls_back.body.bytesize == cover_size + puts 'weak ETag -> 304 on a repeat GET, but If-Range on that same weak ETag correctly falls back to a full 200 ' \ + '(the asymmetry Prism::StaticCache exists to get right)' + + comment_body = URI.encode_www_form( + 'comment[author]' => 'A reader', 'comment[body]' => 'Nice post!', 'authenticity_token' => token + ) + comment = http.post( + "/posts/#{post_id}/comments", comment_body, + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie + ) + puts "POST /posts/#{post_id}/comments -> #{comment.code} Location=#{comment['location']}" + + show_with_comment = http.get(post_path, 'Cookie' => cookie) + raise 'comment did not show up on the post page' unless show_with_comment.body.include?('Nice post!') + puts "GET #{post_path} -> comment thread now includes the new comment" + + comment_id = show_with_comment.body[/id="comment-(\d+)"/, 1] + raise 'could not find the new comment id on the post page' unless comment_id + comment_edit_path = "/posts/#{post_id}/comments/#{comment_id}" + + puts '-' * 40 + puts 'liking the post and one of its comments (polymorphic Like: belongs_to :likeable, polymorphic: true)' + like_post = http.post("/likes?likeable_type=Post&likeable_id=#{post_row_id}", + URI.encode_www_form('authenticity_token' => token), + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + puts "POST /likes?likeable_type=Post&likeable_id=#{post_row_id} -> #{like_post.code} Location=#{like_post['location']}" + raise "expected liking the post to redirect, got #{like_post.code}" unless like_post.code == '302' + like_post_cookie = cookie_from(like_post) + cookie = like_post_cookie unless like_post_cookie.empty? + raise 'Post#likes (as: :likeable) should report exactly 1 like' unless Post.find_by!(slug: post_id).likes.count == 1 + unless Post.find_by!(slug: post_id).likes_count == 1 + raise 'belongs_to :likeable, counter_cache: true should have bumped Post#likes_count to 1 on Like#create' + end + + show_with_like = http.get(post_path, 'Cookie' => cookie) + unless show_with_like.body.include?('1 like') || show_with_like.body.include?('Polubienia: 1') + raise 'post page should render the updated like count' + end + puts "GET #{post_path} -> like count rendered (ApplicationController#likes_count now reads the " \ + "counter_cache column, not record.likes.count), Unlike button now shown" + + like_comment = http.post("/likes?likeable_type=Comment&likeable_id=#{comment_id}", + URI.encode_www_form('authenticity_token' => token), + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + raise "expected liking the comment to redirect, got #{like_comment.code}" unless like_comment.code == '302' + like_comment_cookie = cookie_from(like_comment) + cookie = like_comment_cookie unless like_comment_cookie.empty? + raise 'Comment#likes (as: :likeable) should report exactly 1 like' unless Comment.find(comment_id).likes.count == 1 + unless Comment.find(comment_id).likes_count == 1 + raise 'polymorphic counter_cache should also bump Comment#likes_count, same Like model/column default' + end + puts "POST /likes?likeable_type=Comment&likeable_id=#{comment_id} -> #{like_comment.code} (comment liked too, " \ + "same Like model and controller action as the post)" + + duplicate_like = http.post("/likes?likeable_type=Post&likeable_id=#{post_row_id}", + URI.encode_www_form('authenticity_token' => token), + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + unless duplicate_like.code == '302' + raise "expected a same-user duplicate like to still just redirect quietly, got #{duplicate_like.code}" + end + raise 'validates_uniqueness_of scope: should have silently rejected the duplicate like' unless Post.find_by!(slug: post_id).likes.count == 1 + unless Post.find_by!(slug: post_id).likes_count == 1 + raise 'a rejected duplicate like (failed validation, no Like row created) must not bump likes_count' + end + puts "POST /likes?likeable_type=Post&likeable_id=#{post_row_id} again -> #{duplicate_like.code} " \ + "(uniqueness scope: [:likeable_id, :likeable_type] rejected the duplicate, still 1 like)" + + own_like_id = Post.find_by!(slug: post_id).likes.first.id + unlike = http.post("/likes/#{own_like_id}", URI.encode_www_form('_method' => 'DELETE', 'authenticity_token' => token), + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + raise "expected unliking the post to redirect, got #{unlike.code}" unless unlike.code == '302' + unlike_cookie = cookie_from(unlike) + cookie = unlike_cookie unless unlike_cookie.empty? + raise 'Post#likes should be back to zero after unliking' unless Post.find_by!(slug: post_id).likes.count.zero? + unless Post.find_by!(slug: post_id).likes_count.zero? + raise 'unliking (really_destroy! per LikesController#destroy) should have decremented likes_count back to 0' + end + puts "POST /likes/#{own_like_id} with _method=DELETE -> #{unlike.code} (unliked, count back to 0)" + + relike = http.post("/likes?likeable_type=Post&likeable_id=#{post_row_id}", + URI.encode_www_form('authenticity_token' => token), + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + raise "expected re-liking the post to redirect, got #{relike.code}" unless relike.code == '302' + relike_cookie = cookie_from(relike) + cookie = relike_cookie unless relike_cookie.empty? + puts "POST /likes?likeable_type=Post&likeable_id=#{post_row_id} -> #{relike.code} " \ + "(re-liked so the dependent :destroy cascade below has something to prove)" + + comment_edit_page = http.get("#{comment_edit_path}/edit", 'Cookie' => cookie) + puts "GET #{comment_edit_path}/edit -> #{comment_edit_page.code}" + raise 'comment edit form should be pre-filled with the current body' unless comment_edit_page.body.include?('Nice post!') + + comment_update_body = URI.encode_www_form('comment[body]' => 'Nice post, edited!', '_method' => 'PATCH', + 'authenticity_token' => token) + comment_update = http.post(comment_edit_path, comment_update_body, + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + puts "POST #{comment_edit_path} with _method=PATCH -> #{comment_update.code} Location=#{comment_update['location']}" + raise "expected the comment update to redirect back to the post, got #{comment_update.code}" unless comment_update.code == '302' + comment_update_cookie = cookie_from(comment_update) + cookie = comment_update_cookie unless comment_update_cookie.empty? + + show_with_edited_comment = http.get(post_path, 'Cookie' => cookie) + raise 'edited comment body did not appear on the show page' unless show_with_edited_comment.body.include?('Nice post, edited!') end server.stop