mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-09 08:10:08 +09:00
d485db6038
Each push notification carries the subscriber's unread room count as its badge. The pool built it per subscription, loading the user and counting their unread memberships: two queries for every subscriber, all in the job before the deliveries reach the threads. With 1,000 subscribed members the job spent ~180 ms and 2,000 queries there; with 5,000, a second. The pool now counts the unread rooms of a whole batch with one grouped query and hands each subscription its badge; nothing else in the notification needs the user. The queries still run before the work is posted to the threads, which run outside the Rails executor. Push::Subscription#notification still counts by itself when no badge is given, as for the test notification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142qgjggdJ2KDdGk7RF9Xm9
162 lines
6.7 KiB
Ruby
162 lines
6.7 KiB
Ruby
require "test_helper"
|
|
|
|
class Push::SubscriptionTest < ActiveSupport::TestCase
|
|
setup do
|
|
stub_web_push_dns_resolution
|
|
end
|
|
|
|
test "valid subscription with permitted endpoint" do
|
|
assert build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123").valid?
|
|
end
|
|
|
|
test "rejects endpoint with non-https scheme" do
|
|
subscription = build_subscription(endpoint: "http://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "must use HTTPS"
|
|
end
|
|
|
|
test "rejects endpoint with non-permitted host" do
|
|
subscription = build_subscription(endpoint: "https://attacker.example.com/webhook")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "is not a permitted push service"
|
|
end
|
|
|
|
test "rejects endpoint whose host only suffix-matches a permitted host" do
|
|
subscription = build_subscription(endpoint: "https://evilfcm.googleapis.com.attacker.example/webhook")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "is not a permitted push service"
|
|
end
|
|
|
|
test "rejects blank endpoint" do
|
|
subscription = build_subscription(endpoint: "")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "can't be blank"
|
|
end
|
|
|
|
test "rejects endpoint on a non-default port" do
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com:8443/fcm/send/abc123")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "must use the default HTTPS port"
|
|
end
|
|
|
|
test "rejects endpoint that resolves to private IP" do
|
|
stub_dns_resolution("192.168.1.1")
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "resolves to a private or invalid IP address"
|
|
end
|
|
|
|
test "rejects endpoint that resolves to loopback IP" do
|
|
stub_dns_resolution("127.0.0.1")
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "resolves to a private or invalid IP address"
|
|
end
|
|
|
|
test "rejects endpoint that resolves to link-local IP (AWS IMDS)" do
|
|
stub_dns_resolution("169.254.169.254")
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "resolves to a private or invalid IP address"
|
|
end
|
|
|
|
test "rejects endpoint whose host resolves to nothing without raising" do
|
|
stub_dns_failure
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_nil subscription.resolved_endpoint_ip
|
|
assert_not subscription.valid?
|
|
assert_includes subscription.errors[:endpoint], "resolves to a private or invalid IP address"
|
|
end
|
|
|
|
test "resolved_endpoint_ip returns the pinned public IP" do
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_equal DnsTestHelper::WEB_PUSH_PUBLIC_TEST_IP, subscription.resolved_endpoint_ip
|
|
end
|
|
|
|
test "endpoint resolution is deferred from the enqueue path to the delivery worker" do
|
|
lookups = 0
|
|
# A side-effecting matcher lets us count resolver calls without a real lookup.
|
|
Resolv.stubs(:getaddresses).with { |*| lookups += 1; true }.returns([ DnsTestHelper::WEB_PUSH_PUBLIC_TEST_IP ])
|
|
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
notification = subscription.notification(title: "t", body: "b", path: "/")
|
|
assert_equal 0, lookups, "building the notification must not resolve DNS on the serial enqueue path"
|
|
|
|
WebPush.stubs(:payload_send)
|
|
notification.deliver
|
|
assert_operator lookups, :>, 0, "delivery must resolve and pin the endpoint IP on the worker"
|
|
end
|
|
|
|
test "delivery is skipped when the endpoint no longer resolves to a public IP" do
|
|
stub_dns_resolution("10.0.0.5") # host now answers with a private address
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
assert_nil subscription.resolved_endpoint_ip
|
|
WebPush.expects(:payload_send).never
|
|
subscription.notification(title: "t", body: "b", path: "/").deliver
|
|
end
|
|
|
|
test "delivery is skipped for a non-permitted host even when it resolves publicly" do
|
|
# A row that predates endpoint validation: its host resolves to a public IP,
|
|
# but it is not a permitted push service, so delivery must not proceed.
|
|
subscription = build_subscription(endpoint: "https://attacker.example.com/collect")
|
|
|
|
assert_nil subscription.resolved_endpoint_ip
|
|
WebPush.expects(:payload_send).never
|
|
subscription.notification(title: "t", body: "b", path: "/").deliver
|
|
end
|
|
|
|
test "delivery is skipped for a permitted host on a non-default port" do
|
|
# A legacy/bypassed row: permitted host, resolves publicly, but port 22.
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com:22/fcm/send/abc123")
|
|
|
|
assert_nil subscription.resolved_endpoint_ip
|
|
WebPush.expects(:payload_send).never
|
|
subscription.notification(title: "t", body: "b", path: "/").deliver
|
|
end
|
|
|
|
test "the badge defaults to the subscriber's unread room count" do
|
|
memberships(:david_pets).update! unread_at: Time.current
|
|
memberships(:david_hq).update! unread_at: Time.current
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
WebPush.expects(:payload_send).with { |options| JSON.parse(options[:message]).dig("options", "data", "badge") == 2 }
|
|
subscription.notification(title: "t", body: "b", path: "/").deliver
|
|
end
|
|
|
|
test "delivery sends with the pinned endpoint_ip" do
|
|
subscription = build_subscription(endpoint: "https://fcm.googleapis.com/fcm/send/abc123")
|
|
|
|
WebPush.expects(:payload_send).with(has_entry(endpoint_ip: DnsTestHelper::WEB_PUSH_PUBLIC_TEST_IP))
|
|
subscription.notification(title: "t", body: "b", path: "/").deliver
|
|
end
|
|
|
|
test "accepts all permitted push service domains" do
|
|
[
|
|
"https://fcm.googleapis.com/fcm/send/token123",
|
|
"https://jmt17.google.com/fcm/send/token123",
|
|
"https://updates.push.services.mozilla.com/wpush/v2/token123",
|
|
"https://web.push.apple.com/QaBC123",
|
|
"https://wns2-db5p.notify.windows.com/w/?token=abc123"
|
|
].each do |endpoint|
|
|
subscription = build_subscription(endpoint: endpoint)
|
|
assert subscription.valid?, "Expected #{endpoint} to be valid, got: #{subscription.errors.full_messages}"
|
|
end
|
|
end
|
|
|
|
private
|
|
def build_subscription(endpoint:)
|
|
Push::Subscription.new(user: users(:david), endpoint: endpoint, p256dh_key: "test_key", auth_key: "test_auth")
|
|
end
|
|
end
|