mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
A push subscription belongs to the user, not a session, so a device
signed out by a password change kept getting notifications. Changing
the password now removes every subscription along with the sessions,
in the same transaction.
A browser still signed in keeps its own subscription in its service
worker, so the notifications controller posts it once per session: the
bell names the session by a digest of its id, never its token, and the
browser remembers the last one it posted for. That post never
unsubscribes the browser when the server refuses it.
A request that authenticated just before the change could put a
subscription back after it. The create action now checks the session
still exists and saves in one immediate transaction, after validating
the endpoint outside it.
From #371.
(cherry picked from commit 6cab924097)
126 lines
4.9 KiB
Ruby
126 lines
4.9 KiB
Ruby
require "test_helper"
|
|
|
|
class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
sign_in :david
|
|
stub_web_push_dns_resolution
|
|
end
|
|
|
|
test "create new push subscription" do
|
|
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
|
|
|
|
post user_push_subscriptions_url,
|
|
params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }
|
|
|
|
assert_response :ok
|
|
|
|
assert_equal subscription_params, users(:david).push_subscriptions.last.attributes.slice("endpoint", "p256dh_key", "auth_key")
|
|
assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
|
|
end
|
|
|
|
test "touch existing subscription" do
|
|
assert_no_difference -> { users(:david).push_subscriptions.count } do
|
|
assert_changes -> { push_subscriptions(:david_chrome).reload.updated_at } do
|
|
post user_push_subscriptions_url(params: {
|
|
push_subscription: push_subscriptions(:david_chrome).attributes.slice("endpoint", "p256dh_key", "auth_key")
|
|
})
|
|
end
|
|
end
|
|
|
|
assert_response :ok
|
|
end
|
|
|
|
test "rejects subscription with non-permitted endpoint" do
|
|
subscription_params = { "endpoint" => "https://attacker.example.com/steal", "p256dh_key" => "123", "auth_key" => "456" }
|
|
|
|
assert_no_difference -> { Push::Subscription.count } do
|
|
post user_push_subscriptions_url, params: { push_subscription: subscription_params }
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "rejects subscription with endpoint resolving to a private IP" do
|
|
stub_dns_resolution("169.254.169.254")
|
|
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
|
|
|
|
assert_no_difference -> { Push::Subscription.count } do
|
|
post user_push_subscriptions_url, params: { push_subscription: subscription_params }
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "re-registering a legacy invalid subscription is rejected with 422" do
|
|
# A row that predates endpoint validation (saved without validation, as a
|
|
# sink planted before this shipped could be). Re-POSTing its params must hit
|
|
# the same 422 as a fresh create, not be kept alive by touch.
|
|
legacy = users(:david).push_subscriptions.build \
|
|
endpoint: "https://attacker.example.com/steal", p256dh_key: "123", auth_key: "456"
|
|
legacy.save!(validate: false)
|
|
|
|
assert_no_difference -> { Push::Subscription.count } do
|
|
post user_push_subscriptions_url, params: {
|
|
push_subscription: { endpoint: "https://attacker.example.com/steal", p256dh_key: "123", auth_key: "456" }
|
|
}
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "destroy a push subscription via dev mode" do
|
|
assert_difference -> { Push::Subscription.count }, -1 do
|
|
delete user_push_subscription_url(push_subscriptions(:david_chrome))
|
|
assert_redirected_to user_push_subscriptions_url
|
|
end
|
|
end
|
|
|
|
test "a signed-out session can't register a subscription" do
|
|
Session.delete_all
|
|
|
|
assert_no_difference -> { Push::Subscription.count } do
|
|
post user_push_subscriptions_url, params: { push_subscription: new_subscription_params }
|
|
end
|
|
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
test "a session signed out while its request is in flight doesn't register a subscription" do
|
|
Push::Subscription.any_instance.stubs(:valid?).with { Session.delete_all; true }.returns(true)
|
|
|
|
assert_no_difference -> { Push::Subscription.count } do
|
|
post user_push_subscriptions_url, params: { push_subscription: new_subscription_params }
|
|
end
|
|
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "a session signed out while its request is in flight doesn't keep an existing subscription alive" do
|
|
subscription = push_subscriptions(:david_chrome)
|
|
Push::Subscription.any_instance.stubs(:valid?).with { Session.delete_all; true }.returns(true)
|
|
|
|
assert_no_changes -> { subscription.reload.updated_at } do
|
|
post user_push_subscriptions_url, params: { push_subscription: subscription.attributes.slice("endpoint", "p256dh_key", "auth_key") }
|
|
end
|
|
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "a room names its session for the browser to post its subscription again in a new one" do
|
|
get room_url(rooms(:watercooler))
|
|
first_marker = css_select("[data-notifications-session-value]").first["data-notifications-session-value"]
|
|
|
|
assert_match(/\A\h{64}\z/, first_marker)
|
|
assert_not_includes response.body, users(:david).sessions.order(:id).last.token
|
|
|
|
sign_in :david
|
|
get room_url(rooms(:watercooler))
|
|
assert_not_equal first_marker, css_select("[data-notifications-session-value]").first["data-notifications-session-value"]
|
|
end
|
|
|
|
private
|
|
def new_subscription_params
|
|
{ "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
|
|
end
|
|
end
|