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
|
'user[email]' => 'writer@example.com', 'user[password]' => 'wrong password', 'authenticity_token' => token
|
||||||
)
|
)
|
||||||
# Three POST /session slots are already spent before this loop starts:
|
# 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
|
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
|
server.stop
|
||||||
puts '-' * 40
|
puts '-' * 40
|
||||||
puts 'blog demo done (process about to exit; data stays on disk for the restart check)'
|
puts 'blog demo done (process about to exit; data stays on disk for the restart check)'
|
||||||
Reference in new issue
Block a user