mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
Changing the password is how someone shuts out whoever else is signed in to their account, but every session opened with the old password stayed valid. Now the others end and their live connections close, as on sign-out. The session the password was changed from stays signed in. Changing the password outside a request, as from a console, ends every session.
218 lines
8.5 KiB
Ruby
218 lines
8.5 KiB
Ruby
require "test_helper"
|
|
|
|
class CachedResponsesTest < ActionDispatch::IntegrationTest
|
|
self.use_transactional_tests = false
|
|
|
|
setup do
|
|
host! "once.campfire.test"
|
|
sign_in :david
|
|
@login_cookie = cookies["_campfire_session"]
|
|
@previous_forgery = ActionController::Base.allow_forgery_protection
|
|
ActionController::Base.allow_forgery_protection = true
|
|
@previous_cache = Rails.cache
|
|
@previous_caching = ActionController::Base.perform_caching
|
|
Rails.cache = ActiveSupport::Cache::MemoryStore.new
|
|
ActionController::Base.perform_caching = true
|
|
@room = rooms(:watercooler)
|
|
# Establish last-room cookies before checking reuse.
|
|
2.times { get room_url(@room) }
|
|
ResponseCache.instance.clear
|
|
end
|
|
|
|
teardown do
|
|
ResponseCache.instance.clear
|
|
ActionController::Base.allow_forgery_protection = @previous_forgery
|
|
Rails.cache = @previous_cache
|
|
ActionController::Base.perform_caching = @previous_caching
|
|
end
|
|
|
|
test "a reused page carries the same enforcing policy, with no nonce" do
|
|
get room_messages_url(@room)
|
|
first = response.headers["Content-Security-Policy"]
|
|
assert_match %r{script-src [^/ ]+/assets/ 'sha256-}, first
|
|
assert_no_match "nonce-", first
|
|
|
|
MessagesController.any_instance.expects(:find_paged_messages).never
|
|
get room_messages_url(@room)
|
|
assert_response :success
|
|
assert_equal first, response.headers["Content-Security-Policy"]
|
|
end
|
|
|
|
test "all four read actions reuse completed pages" do
|
|
urls = [ room_url(@room), room_messages_url(@room), user_sidebar_url(:me), searches_url(q: "hello") ]
|
|
urls.each do |url|
|
|
get url
|
|
assert_response :success
|
|
end
|
|
ResponseCache.instance.expects(:write).never
|
|
urls.each do |url|
|
|
get url
|
|
assert_response :success
|
|
end
|
|
end
|
|
|
|
test "pagination caches the completed HTML rather than silently bypassing admission" do
|
|
get room_messages_url(@room)
|
|
assert_response :success
|
|
assert_equal "text/html", response.media_type
|
|
MessagesController.any_instance.expects(:find_paged_messages).never
|
|
get room_messages_url(@room)
|
|
assert_response :success
|
|
end
|
|
|
|
test "clients without token state reuse complete HTML and post without tokens" do
|
|
cookies["_campfire_session"] = @login_cookie
|
|
get room_url(@room)
|
|
first = response.body
|
|
assert_select 'meta[name="csrf-token"]', count: 0
|
|
assert_select 'input[name="authenticity_token"]', count: 0
|
|
cookies["_campfire_session"] = @login_cookie
|
|
ResponseCache.instance.expects(:write).never
|
|
get room_url(@room)
|
|
assert_response :success
|
|
assert_equal first, response.body
|
|
post room_messages_url(@room, format: :turbo_stream), params: {
|
|
message: { body: "tokenless replay works", client_message_id: "cache-replay" } },
|
|
headers: { "Sec-Fetch-Site" => "same-origin", "Origin" => "http://once.campfire.test" }
|
|
assert_response :success
|
|
end
|
|
|
|
test "literal token-like text survives complete page reuse" do
|
|
literal = "campfire-csrf-literal authenticity_token csrf-token"
|
|
@room.messages.create!(creator: users(:david), body: "literal #{literal}")
|
|
get room_url(@room)
|
|
first = response.body
|
|
get room_url(@room)
|
|
assert_equal first, response.body
|
|
assert_includes response.body, "literal #{literal}"
|
|
end
|
|
|
|
test "gzip bytes are reused and identity negotiation stays separate" do
|
|
require "stringio"
|
|
get room_url(@room), headers: { "Accept-Encoding" => "gzip" }
|
|
assert_equal "gzip", response.headers["Content-Encoding"]
|
|
encoded = response.body
|
|
decoded = Zlib::GzipReader.new(StringIO.new(encoded)).read
|
|
assert_includes decoded, "<html"
|
|
assert_includes response.headers["Vary"], "Accept-Encoding"
|
|
get room_url(@room)
|
|
assert_equal decoded, response.body
|
|
ResponseCache.instance.expects(:write).never
|
|
get room_url(@room), headers: { "Accept-Encoding" => "gzip" }
|
|
assert_equal encoded, response.body
|
|
get room_url(@room)
|
|
assert_nil response.headers["Content-Encoding"]
|
|
assert_equal decoded, response.body
|
|
get room_url(@room), headers: { "Accept-Encoding" => "gzip;q=0, identity;q=1" }
|
|
assert_nil response.headers["Content-Encoding"]
|
|
assert_equal decoded, response.body
|
|
get room_url(@room), headers: { "Accept-Encoding" => "*;q=0" }
|
|
assert_nil response.headers["Content-Encoding"]
|
|
end
|
|
|
|
test "local and foreign commits invalidate pages and nested fragments" do
|
|
message = @room.messages.ordered.last
|
|
get room_url(@room)
|
|
@room.messages.create!(creator: users(:david), body: "local commit")
|
|
get room_url(@room)
|
|
assert_includes response.body, "local commit"
|
|
|
|
foreign_write("UPDATE users SET name = ? WHERE id = ?", "Foreign creator", message.creator_id)
|
|
get room_url(@room)
|
|
assert_includes response.body, "Foreign creator"
|
|
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign message body", message.id)
|
|
get room_url(@room)
|
|
assert_includes response.body, "foreign message body"
|
|
end
|
|
|
|
test "cached room access and sessions are checked afresh" do
|
|
get room_url(@room)
|
|
foreign_write("DELETE FROM memberships WHERE room_id = ? AND user_id = ?", @room.id, users(:david).id)
|
|
get room_url(@room)
|
|
assert_redirected_to root_url
|
|
get user_sidebar_url(:me)
|
|
foreign_write("DELETE FROM sessions WHERE user_id = ?", users(:david).id)
|
|
get user_sidebar_url(:me)
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
test "a session signed out by a password change isn't served from a cached snapshot or page" do
|
|
other_device = open_session { |session| session.host! "once.campfire.test" }
|
|
other_device.post session_url, params: { email_address: users(:david).email_address, password: "secret123456" }
|
|
other_device.get user_sidebar_url(:me)
|
|
other_device.assert_response :success
|
|
|
|
Session.expects(:find_by).never
|
|
ResponseCache.instance.expects(:write).never
|
|
other_device.get user_sidebar_url(:me)
|
|
other_device.assert_response :success
|
|
Session.unstub(:find_by)
|
|
ResponseCache.instance.unstub(:write)
|
|
|
|
put user_profile_url, params: { user: { password: "a new secret123456" } },
|
|
headers: { "Sec-Fetch-Site" => "same-origin", "Origin" => "http://once.campfire.test" }
|
|
assert_redirected_to user_profile_url
|
|
|
|
other_device.get user_sidebar_url(:me)
|
|
other_device.assert_redirected_to new_session_url
|
|
get user_sidebar_url(:me)
|
|
assert_response :success
|
|
end
|
|
|
|
test "conditional requests keep native validators" do
|
|
get room_messages_url(@room)
|
|
etag = response.headers["ETag"]
|
|
assert etag.present?
|
|
get room_messages_url(@room), headers: { "If-None-Match" => etag }
|
|
assert_response :not_modified
|
|
end
|
|
|
|
test "foreign presentation changes cannot return a false not-modified response" do
|
|
get room_messages_url(@room)
|
|
etag = response.headers["ETag"]
|
|
message = @room.messages.ordered.last
|
|
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "changed presentation", message.id)
|
|
get room_messages_url(@room), headers: { "If-None-Match" => etag }
|
|
assert_response :success
|
|
assert_includes response.body, "changed presentation"
|
|
assert_not_equal etag, response.headers["ETag"]
|
|
end
|
|
|
|
test "origins and frame variants have separate entries" do
|
|
get room_url(@room)
|
|
ResponseCache.instance.expects(:write).twice
|
|
get room_url(@room), headers: { "Turbo-Frame" => "different-frame" }
|
|
host! "once.campfire.test:8081"
|
|
get room_path(@room)
|
|
assert_response :success
|
|
end
|
|
|
|
test "cache can be disabled" do
|
|
ResponseCache.instance.stubs(:budget).returns(0)
|
|
ResponseCache.instance.expects(:read).never
|
|
2.times { get room_url(@room) }
|
|
assert_response :success
|
|
end
|
|
|
|
test "a commit after authentication cannot admit captured user fields" do
|
|
Users::SidebarsController.any_instance.stubs(:set_version_headers).with do
|
|
foreign_write("UPDATE users SET name = ? WHERE id = ?", "During authentication", users(:david).id)
|
|
true
|
|
end
|
|
get user_sidebar_url(:me)
|
|
Users::SidebarsController.any_instance.unstub(:set_version_headers)
|
|
ResponseCache.instance.expects(:write).once
|
|
get user_sidebar_url(:me)
|
|
assert_includes response.body, "During authentication"
|
|
end
|
|
|
|
private
|
|
def foreign_write(sql, *bindings)
|
|
SQLite3::Database.new(ActiveRecord::Base.connection_db_config.database) do |database|
|
|
database.execute(sql, bindings)
|
|
end
|
|
# Fixtures share this thread's query cache outside the request executor.
|
|
ActiveRecord::Base.clear_query_caches_for_current_thread
|
|
end
|
|
end
|