feat: grow blog_demo
This commit is contained in:
1 parent
889030db77
commit
8a31d9de17
1 file changed
+89
@@ -1894,8 +1894,97 @@ Net::HTTP.start('127.0.0.1', port) do |http|
|
||||
'user[email]' => 'writer@example.com', 'user[password]' => 'wrong password', 'authenticity_token' => token
|
||||
)
|
||||
# Three POST /session slots are already spent before this loop starts:
|
||||
# device A's own sign-in plus devices B and C from the multi-device
|
||||
# remember-me scene above (each a real POST /session.json, all from the
|
||||
# same IP). So the throttle (limit: 5) trips two attempts earlier here
|
||||
# than it would against a completely fresh IP. Attempt 1 goes through
|
||||
# .json instead of the usual form post - same slot either way (the
|
||||
# discriminator strips the extension), but it doubles as the
|
||||
# JSON-format proof that a rejected login is a 401 with a JSON:API
|
||||
# error, not the HTML branch's 422.
|
||||
6.times do |i|
|
||||
path = i.zero? ? '/session.json' : '/session'
|
||||
attempt = http.post(path, login_attempt_body, 'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'Cookie' => cookie)
|
||||
expected = if i.zero? then '401'
|
||||
elsif i == 1 then '422'
|
||||
else '429'
|
||||
end
|
||||
puts " attempt #{i + 1}: POST #{path} -> #{attempt.code} (expected #{expected})"
|
||||
raise "login attempt #{i + 1}: expected #{expected}, got #{attempt.code}" unless attempt.code == expected
|
||||
if i.zero?
|
||||
error = JSON.parse(attempt.body)['errors'].first
|
||||
raise 'expected the JSON login rejection to point at the password attribute' unless error['source']['pointer'] == '/data/attributes/password'
|
||||
end
|
||||
raise 'the blocked attempt should carry a Retry-After header' if i == 2 && attempt['retry-after'].nil?
|
||||
end
|
||||
|
||||
puts '-' * 40
|
||||
puts 'hammering POST /users to exercise the signup throttle (limit 3/60s per IP)'
|
||||
signup_attempt_body = URI.encode_www_form(
|
||||
'user[email]' => 'writer@example.com', 'user[password]' => 'irrelevant',
|
||||
'user[password_confirmation]' => 'irrelevant', 'authenticity_token' => token
|
||||
)
|
||||
# The demo user and the ownership-check user above each cost one of the
|
||||
# 3 POST /users slots, so the throttle trips one attempt earlier here
|
||||
# than it would against a completely fresh IP. Attempt 1 goes through
|
||||
# .json to prove the duplicate-email rejection is a normal render_errors
|
||||
# 422 (same shape a JSON client already gets from posts.json), not a
|
||||
# special case just because it happens to come from a throttled action.
|
||||
2.times do |i|
|
||||
path = i.zero? ? '/users.json' : '/users'
|
||||
attempt = http.post(path, signup_attempt_body, 'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'Cookie' => cookie)
|
||||
expected = i.zero? ? '422' : '429'
|
||||
puts " attempt #{i + 1}: POST #{path} -> #{attempt.code} (expected #{expected})"
|
||||
raise "signup attempt #{i + 1}: expected #{expected}, got #{attempt.code}" unless attempt.code == expected
|
||||
if i.zero?
|
||||
error = JSON.parse(attempt.body)['errors'].first
|
||||
raise 'expected the JSON signup rejection to point at the email attribute' unless error['source']['pointer'] == '/data/attributes/email'
|
||||
end
|
||||
raise 'the blocked attempt should carry a Retry-After header' if i == 1 && attempt['retry-after'].nil?
|
||||
end
|
||||
end
|
||||
|
||||
puts '-' * 40
|
||||
puts "checking that add_index :users, :email, unique: true (db/migrate/20260907000002_add_index_on_users_email.rb) is a REAL storage-level constraint, not just the app-level validates :email, uniqueness: true above"
|
||||
existing_writer = User.find_by(email: 'writer@example.com')
|
||||
raise 'expected the writer@example.com user created earlier in this demo to still exist' unless existing_writer
|
||||
users_count_before_bypass = User.count
|
||||
# Model#save always calls #valid? first, which is exactly what the app-level
|
||||
# uniqueness validator above already covers - so the only way to prove the
|
||||
# INDEX (not the validator) is what actually stops a duplicate is to skip
|
||||
# the model layer entirely and hit Store#insert directly, the way a future
|
||||
# admin script, a data migration, or a second process racing past the
|
||||
# validator's own SELECT before either commits, all would.
|
||||
begin
|
||||
User.store.insert(User.table_name, 'email' => existing_writer[:email])
|
||||
raise 'expected the unique index on users.email to reject a duplicate row inserted directly through Store#insert, bypassing Model#valid? entirely'
|
||||
rescue Prism::Orm::Store::UniqueConstraintViolation => e
|
||||
puts " Store#insert with a duplicate encrypted email, bypassing Model validation entirely -> #{e.class}: #{e.message}"
|
||||
end
|
||||
raise 'the rejected bypass insert must not have left a phantom row behind' unless User.count == users_count_before_bypass
|
||||
|
||||
puts '-' * 40
|
||||
puts 'checking examples/blog/log/development.log for real Instrumentation::LogSubscriber output'
|
||||
new_log_lines = File.read(log_path)[log_offset_before_requests..].to_s
|
||||
%w[sql.prism render_template.prism process_action.prism perform_job.prism deliver_mail.prism].each do |event_name|
|
||||
raise "expected at least one #{event_name} line in #{log_path}" unless new_log_lines.include?(" #{event_name} (")
|
||||
end
|
||||
unless new_log_lines.include?('table="posts"')
|
||||
raise "expected a sql.prism line touching the posts table in #{log_path}"
|
||||
end
|
||||
unless new_log_lines.include?('path="/"')
|
||||
raise "expected a process_action.prism line for the homepage in #{log_path}"
|
||||
end
|
||||
unless new_log_lines.include?('job_class="Prism::Mailer::DeliveryJob"')
|
||||
raise "expected a perform_job.prism line for the mailer delivery job in #{log_path}"
|
||||
end
|
||||
unless new_log_lines.include?('deliver_mail.prism') && new_log_lines.include?('delivery_method=:test')
|
||||
raise "expected a deliver_mail.prism line for the notification email in #{log_path}"
|
||||
end
|
||||
puts " log grew by #{new_log_lines.lines.count} lines, sql/render/process_action/perform_job/deliver_mail all present"
|
||||
|
||||
server.stop
|
||||
puts '-' * 40
|
||||
puts 'blog demo done (process about to exit; data stays on disk for the restart check)'
|
||||
Reference in new issue
Block a user