diff --git a/blog_demo.rb b/blog_demo.rb
index b2585c5..a56f24a 100644
--- a/blog_demo.rb
+++ b/blog_demo.rb
@@ -1,21 +1,168 @@
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
require 'prism'
require 'net/http'
+require 'zlib'
+require 'stringio'
+require 'csv'
require_relative 'examples/blog/app'
server = Prism::Server.new(Blog, port: 0).start
port = server.port
puts "blog demo server on port #{port}"
puts "database file: #{File.join(__dir__, 'examples/blog/db/development.pstore')}"
+log_path = File.join(__dir__, 'examples/blog/log/development.log')
+log_offset_before_requests = File.exist?(log_path) ? File.size(log_path) : 0
puts '-' * 40
+def cookie_from(response)
+ lines = response.get_fields('set-cookie') || []
+ lines.map { |line| line.split(';').first }.join('; ')
+end
+
+def cookie_value_from(response, name)
+ lines = response.get_fields('set-cookie') || []
+ line = lines.find { |l| l.start_with?("#{name}=") }
+ line&.split(';')&.first
+end
+
+def csrf_token_from(html)
+ html[/name="csrf-token" content="([^"]+)"/, 1]
+end
+
+def multipart_body(boundary, fields:, files: {})
+ parts = fields.map do |name, value|
+ "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{name}\"\r\n\r\n#{value}\r\n"
+ end
+ parts += files.map do |name, (filename, content_type, content)|
+ "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n" \
+ "Content-Type: #{content_type}\r\n\r\n#{content}\r\n"
+ end
+ parts.join + "--#{boundary}--\r\n"
+end
+
+# Signs in as the writer from a brand new, entirely independent cookie jar
+# (its own session cookie, its own CSRF token fetched fresh from the
+# sign-in page) - a second real HTTP client would look exactly like this,
+# unlike reusing the caller's own `cookie`/`token`. Returns the
+# remember_session_token cookie this "browser" got back, proving
+# Prism::Authenticatable's remember_sessions: hands out one independent
+# LoginSession row per device instead of one shared column.
+def sign_in_new_device(http, label, user_agent:)
+ new_session_page = http.get('/session/new', 'User-Agent' => user_agent)
+ device_cookie = cookie_from(new_session_page)
+ device_token = csrf_token_from(new_session_page.body)
+ body = URI.encode_www_form(
+ 'user[email]' => 'writer@example.com', 'user[password]' => 'correct horse battery staple',
+ 'user[remember_me]' => '1', 'authenticity_token' => device_token
+ )
+ signed_in = http.post('/session.json', body, 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Cookie' => device_cookie, 'User-Agent' => user_agent)
+ raise "expected #{label} to sign in, got #{signed_in.code}" unless signed_in.code == '200'
+ remember_cookie = cookie_value_from(signed_in, 'remember_session_token')
+ raise "expected #{label}'s sign-in to set its own remember_session_token cookie" unless remember_cookie
+ puts " #{label} signed in from its own independent cookie jar -> #{signed_in.code}, own remember-me cookie set"
+ remember_cookie
+end
+
Net::HTTP.start('127.0.0.1', port) do |http|
+ home = http.get('/')
+ cookie = cookie_from(home)
+ token = csrf_token_from(home.body)
+ puts "GET / -> #{home.code} (session cookie set, csrf token captured)"
+
+ css_href = home.body[//, 1]
+ js_src = home.body[/