feat: extend blog_demo with cookie_from and cookie_value_from
This commit is contained in:
1 parent
7c3d06a8eb
commit
56385c1f2e
1 file changed
+150
-3
+150
-3
@@ -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[/<link rel="stylesheet" href="([^"]+)">/, 1]
|
||||
js_src = home.body[/<script src="([^"]+)">/, 1]
|
||||
raise 'expected the layout to reference a fingerprinted stylesheet' unless css_href =~ %r{\A/assets/stylesheets/application-[0-9a-f]{8}\.css\z}
|
||||
raise 'expected the layout to reference a fingerprinted script' unless js_src =~ %r{\A/assets/javascripts/application-[0-9a-f]{8}\.js\z}
|
||||
|
||||
css_response = http.get(css_href)
|
||||
js_response = http.get(js_src)
|
||||
manifest = JSON.parse(File.read(File.join(__dir__, 'examples/blog/public/assets/manifest.json')))
|
||||
unless css_response.code == '200' && css_response['cache-control'] == 'public, max-age=31536000, immutable'
|
||||
raise "expected the fingerprinted stylesheet to be served with a far-future Cache-Control, got #{css_response.code} #{css_response['cache-control'].inspect}"
|
||||
end
|
||||
unless manifest['stylesheets/application.css'] == css_href.delete_prefix('/assets/')
|
||||
raise 'expected the on-disk manifest.json to agree with the href the layout rendered'
|
||||
end
|
||||
puts "GET #{css_href} -> #{css_response.code}, Cache-Control=#{css_response['cache-control']}"
|
||||
puts "GET #{js_src} -> #{js_response.code} (Prism::Assets::Compiler fingerprinted both files, manifest.json on disk matches)"
|
||||
|
||||
puts '-' * 40
|
||||
puts 'Conditional GET (Prism::StaticCache, RFC 7232/7233) against the fingerprinted stylesheet, over the ' \
|
||||
'real Prism::Server socket - Middleware::Assets hands out a STRONG ETag here (derived from the ' \
|
||||
'fingerprint hash already in the filename), the only place in this app where If-Range after an ETag can succeed'
|
||||
css_etag = css_response['etag']
|
||||
raise "expected a strong ETag (no W/ prefix) on the fingerprinted stylesheet, got #{css_etag.inspect}" if css_etag.nil? || css_etag.start_with?('W/')
|
||||
css_not_modified = http.get(css_href, 'If-None-Match' => css_etag)
|
||||
puts "GET #{css_href} with If-None-Match: #{css_etag} -> #{css_not_modified.code}"
|
||||
raise "expected 304 for a matching strong ETag, got #{css_not_modified.code}" unless css_not_modified.code == '304'
|
||||
raise 'a 304 must not carry a body' unless css_not_modified.body.to_s.empty?
|
||||
raise 'a 304 must not carry Content-Type (no entity, only validators)' if css_not_modified['content-type']
|
||||
|
||||
css_range_honored = http.get(css_href, 'If-Range' => css_etag, 'Range' => 'bytes=0-3')
|
||||
puts "GET #{css_href} with If-Range: #{css_etag} (strong) + Range: bytes=0-3 -> #{css_range_honored.code}, " \
|
||||
"Content-Range=#{css_range_honored['content-range']}"
|
||||
unless css_range_honored.code == '206'
|
||||
raise "expected 206: If-Range with a matching STRONG ETag must honor Range, got #{css_range_honored.code}"
|
||||
end
|
||||
raise 'expected a bytes 0-3/<total> Content-Range' unless css_range_honored['content-range'] =~ %r{\Abytes 0-3/\d+\z}
|
||||
puts 'strong ETag -> 304 on a repeat GET and Range honored under If-Range, both through the real TCP socket'
|
||||
|
||||
anonymous_attempt = http.post(
|
||||
'/posts', URI.encode_www_form('post[title]' => 'should be blocked', 'authenticity_token' => token),
|
||||
'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie
|
||||
)
|
||||
puts "POST /posts while signed out -> #{anonymous_attempt.code} Location=#{anonymous_attempt['location']}"
|
||||
unless anonymous_attempt.code == '302' && anonymous_attempt['location'].to_s.end_with?('/session/new')
|
||||
raise "expected an anonymous post to be redirected to the sign-in page, got #{anonymous_attempt.code} " \
|
||||
"Location=#{anonymous_attempt['location']}"
|
||||
end
|
||||
|
||||
signup_body = URI.encode_www_form(
|
||||
'user[email]' => 'writer@example.com', 'user[password]' => 'correct horse battery staple',
|
||||
'user[password_confirmation]' => 'correct horse battery staple', 'authenticity_token' => token
|
||||
)
|
||||
signup = http.post('/users', signup_body, 'Content-Type' => 'application/x-www-form-urlencoded', 'Cookie' => cookie)
|
||||
puts "POST /users (sign up) -> #{signup.code} Location=#{signup['location']}"
|
||||
raise "expected sign-up to redirect, got #{signup.code}" unless signup.code == '302'
|
||||
signed_in_cookie = cookie_from(signup)
|
||||
cookie = signed_in_cookie unless signed_in_cookie.empty?
|
||||
|
||||
# User#email is `encrypts :email, deterministic: true` - prove that is
|
||||
# real end to end, not just unit-tested: find_by(email:) still resolves
|
||||
# the plaintext through the real request cycle's created row, but what
|
||||
# actually landed on disk is ciphertext, not "writer@example.com".
|
||||
writer_user = User.find_by(email: 'writer@example.com')
|
||||
raise 'expected to find the newly signed-up user back by plaintext email (deterministic encryption)' unless writer_user
|
||||
writer_raw_row = Prism::Orm.store.find(:users, writer_user.id)
|
||||
if writer_raw_row['email'] == 'writer@example.com' || !writer_raw_row['email'].to_s.include?('--')
|
||||
raise "expected users.email to be stored encrypted at rest, got #{writer_raw_row['email'].inspect}"
|
||||
end
|
||||
puts " users.email stored encrypted at rest (#{writer_raw_row['email'][0, 20]}...), decrypts back to #{writer_user.email}"
|
||||
|
||||
boundary = '----PrismBlogDemoBoundary'
|
||||
body = multipart_body(
|
||||
boundary,
|
||||
fields: { 'post[title]' => 'Hello from Prism', 'post[body]' => 'This record was written through the ORM-lite file store.',
|
||||
'authenticity_token' => token },
|
||||
files: { 'post[cover]' => ['cover.txt', 'text/plain', 'not really an image, just bytes'] }
|
||||
)
|
||||
create = http.post(
|
||||
'/posts',
|
||||
URI.encode_www_form(title: 'Hello from Prism', body: 'This record was written through the ORM-lite file store.'),
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
body,
|
||||
'Content-Type' => "multipart/form-data; boundary=#{boundary}", 'Cookie' => cookie
|
||||
)
|
||||
puts "POST /posts -> #{create.code} Location=#{create['location']}"
|
||||
post_path = create['location']
|
||||
puts "POST /posts (multipart, with cover + CSRF token) -> #{create.code} Location=#{post_path}"
|
||||
raise "expected a redirect to the new post, got #{create.code}" unless create.code == '302'
|
||||
# Every flash-touching response gets a fresh Set-Cookie (the whole
|
||||
# session, flash included, lives signed in the cookie - see
|
||||
# Prism::Middleware::Session#write_cookie), so it has to be captured
|
||||
# here too, not just after sign-in/sign-up/sign-out - otherwise the
|
||||
# edit/update scene below would keep reading the flash frozen into the
|
||||
# cookie from right after sign-up instead of the post's own notices.
|
||||
post_create_cookie = cookie_from(create)
|
||||
cookie = post_create_cookie unless post_create_cookie.empty?
|
||||
|
||||
listing = http.get('/posts')
|
||||
puts "GET /posts -> #{listing.code}"
|
||||
|
||||
Reference in new issue
Block a user