From e63fc6af2ec636c90f7a7aa8f43ba03aa3665645 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 22:40:09 +0200 Subject: [PATCH] feat: implement cookie_from and csrf_token_from --- blog_giveaway_worker.rb | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 blog_giveaway_worker.rb diff --git a/blog_giveaway_worker.rb b/blog_giveaway_worker.rb new file mode 100644 index 0000000..7b1de32 --- /dev/null +++ b/blog_giveaway_worker.rb @@ -0,0 +1,76 @@ +# One entrant's attempt to claim a limited giveaway slot, run as its own, +# completely independent `ruby` process - a real second (third, fourth...) +# `Prism::Server` instance booted against the SAME shared database file as +# every other worker (see blog_giveaway_demo.rb, which spawns several of +# these at once and only then releases them all together), not just +# another thread inside one process. This is the scenario +# ClaimsController#create's `post.with_lock` (Prism::Orm::Pessimistic) has +# to hold up against for real: several independent app-server processes - +# think several Puma/Unicorn workers behind a load balancer, all pointed +# at the same database - racing the same "claim now" button on the same +# post at (as close to) the same instant as this demo can arrange. +# +# ARGV: label email password slug go_file +$LOAD_PATH.unshift(File.expand_path('lib', __dir__)) +require 'prism' +require 'net/http' +require_relative 'examples/blog/app' + +label, email, password, slug, go_file = ARGV + +def cookie_from(response) + (response.get_fields('set-cookie') || []).map { |line| line.split(';').first }.join('; ') +end + +def csrf_token_from(html) + html[/name="csrf-token" content="([^"]+)"/, 1] +end + +server = Prism::Server.new(Blog, port: 0).start +port = server.port + +Net::HTTP.start('127.0.0.1', port) do |http| + sign_in_page = http.get('/session/new') + cookie = cookie_from(sign_in_page) + token = csrf_token_from(sign_in_page.body) + login_body = URI.encode_www_form('user[email]' => email, 'user[password]' => password, 'authenticity_token' => token) + signed_in = http.post('/session', login_body, 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + raise "#{label}: sign-in failed (#{signed_in.code})" unless signed_in.code == '302' + new_cookie = cookie_from(signed_in) + cookie = new_cookie unless new_cookie.empty? + + show_page = http.get("/posts/#{slug}", 'Cookie' => cookie) + raise "#{label}: could not load post page (#{show_page.code})" unless show_page.code == '200' + claim_token = csrf_token_from(show_page.body) + + # Every worker reaches this point at its own pace (fresh Ruby boot, fresh + # Prism::Server, its own sign-in round trip) - spin-waiting on a file the + # demo only creates once ALL workers have been spawned turns "started at + # roughly the same time" into "fires the actual claim request at + # (approximately) the same instant", which is the moment that actually + # needs to race for this demo to mean anything. + sleep(0.01) until File.exist?(go_file) + + claim_body = URI.encode_www_form('authenticity_token' => claim_token) + response = http.post("/posts/#{slug}/claims", claim_body, + 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie) + raise "#{label}: claim request failed (#{response.code})" unless response.code == '302' + # The flash notice this redirect just set lives in the session cookie + # itself (see Prism::Session) - the Set-Cookie on THIS response, not the + # one from the earlier sign-in, is what carries it forward. + post_claim_cookie = cookie_from(response) + cookie = post_claim_cookie unless post_claim_cookie.empty? + + landed = http.get(response['location'], 'Cookie' => cookie) + outcome = + if landed.body.include?('You claimed a giveaway slot!') + 'claimed' + elsif landed.body.include?('Sorry, this giveaway is sold out') + 'sold_out' + elsif landed.body.include?('You already claimed a slot for this giveaway') + 'already_claimed' + else + 'unknown' + end + puts "#{label}:#{outcome}" +end