Files
Marcello Costagliola d485db6038 Count unread rooms for push badges once per batch
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
2026-10-05 16:49:59 +02:00

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