discourse/spec/jobs/deliver_push_notification_spec.rb
Rafael dos Santos Silva 75718576c6
DEV: Centralize push notification delivery into a single job (#39207)
## Summary

- Unifies the two parallel push notification delivery paths (web push
via VAPID and hub/native app relay) into a single
`DeliverPushNotification` job
- Extracts hub push delivery into `HubPushNotificationPusher` service,
symmetric with the existing `PushNotificationPusher` for web push
- Adds `UserApiKey.push_clients_for(user)` to encapsulate hub client
lookup previously inlined in PostAlerter
- Simplifies `PostAlerter.push_notification()` from two parallel enqueue
blocks to a single call
- Converts old jobs (`SendPushNotification`, `PushNotification`) to thin
delegators for Sidekiq backward compatibility with in-flight jobs

## Test plan

- [ ] `bin/rspec spec/jobs/deliver_push_notification_spec.rb` — new
unified job tests
- [ ] `bin/rspec spec/services/hub_push_notification_pusher_spec.rb` —
new hub pusher tests
- [ ] `bin/rspec spec/jobs/send_push_notification_spec.rb
spec/jobs/push_notification_spec.rb` — backward compat delegation
- [ ] `bin/rspec spec/services/post_alerter_spec.rb` — PostAlerter
integration
- [ ] `bin/rspec spec/services/push_notification_pusher_spec.rb` — web
push unchanged
- [ ] `bin/rspec
plugins/chat/spec/jobs/regular/chat/notify_watching_spec.rb
plugins/chat/spec/jobs/regular/chat/notify_mentioned_spec.rb` — chat
plugin callers
2026-04-14 11:55:45 -03:00

190 lines
6.3 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Jobs::DeliverPushNotification do
fab!(:user)
fab!(:post)
let(:payload) do
{
"notification_type" => 1,
"post_url" => "/t/#{post.topic_id}/#{post.post_number}",
"excerpt" => "Hello you",
}
end
before do
freeze_time
SiteSetting.push_notification_time_window_mins = 5
end
describe "time window gate" do
before { Fabricate(:push_subscription, user: user) }
it "does not deliver when user is missing" do
PushNotificationPusher.expects(:push).never
HubPushNotificationPusher.expects(:push).never
Jobs::DeliverPushNotification.new.execute(user_id: -999, payload: payload)
end
it "does not deliver when user was recently seen" do
user.update!(last_seen_at: 1.minute.ago)
PushNotificationPusher.expects(:push).never
HubPushNotificationPusher.expects(:push).never
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
it "delivers when user is offline" do
user.update!(last_seen_at: 10.minutes.ago)
PushNotificationPusher.expects(:push).with(user, payload)
HubPushNotificationPusher.expects(:push).with(user, payload)
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
it "delivers when bypass_time_window is true despite user being active" do
user.update!(last_seen_at: 1.minute.ago)
PushNotificationPusher.expects(:push).with(user, payload)
HubPushNotificationPusher.expects(:push).with(user, payload)
Jobs::DeliverPushNotification.new.execute(
user_id: user.id,
bypass_time_window: true,
payload: payload,
)
end
end
describe "web push delivery" do
before { user.update!(last_seen_at: 10.minutes.ago) }
it "calls PushNotificationPusher when user has push subscriptions" do
Fabricate(:push_subscription, user: user)
PushNotificationPusher.expects(:push).with(user, payload).once
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
it "does not call PushNotificationPusher when user has no push subscriptions" do
PushNotificationPusher.expects(:push).never
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
end
describe "hub push delivery" do
before { user.update!(last_seen_at: 10.minutes.ago) }
it "calls HubPushNotificationPusher" do
HubPushNotificationPusher.expects(:push).with(user, payload).once
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
end
describe "both mechanisms" do
it "delivers via both pushers for the same user" do
user.update!(last_seen_at: 10.minutes.ago)
Fabricate(:push_subscription, user: user)
PushNotificationPusher.expects(:push).with(user, payload).once
HubPushNotificationPusher.expects(:push).with(user, payload).once
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: payload)
end
end
describe "content localization" do
fab!(:topic) { Fabricate(:topic, title: "Original topic title") }
fab!(:localized_post) { Fabricate(:post, topic: topic, raw: "Original post content") }
let(:localizable_payload) do
{
"notification_type" => 1,
"post_url" => "/t/#{topic.id}/#{localized_post.post_number}",
"topic_title" => topic.title,
"topic_id" => topic.id,
"post_id" => localized_post.id,
"excerpt" => "Original post content",
"username" => "system",
"post_number" => localized_post.post_number,
}
end
before do
SiteSetting.content_localization_enabled = true
SiteSetting.allow_user_locale = true
user.update!(last_seen_at: 10.minutes.ago, locale: "ja")
end
it "localizes payload before delivering to web push" do
Fabricate(:push_subscription, user: user)
Fabricate(
:topic_localization,
topic: topic,
locale: "ja",
title: "ローカライズされたトピック",
fancy_title: "ローカライズされたトピック",
)
Fabricate(
:post_localization,
post: localized_post,
locale: "ja",
raw: "ローカライズされた投稿",
cooked: "<p>ローカライズされた投稿</p>",
)
PushNotificationPusher
.expects(:push)
.with { |u, p| p[:topic_title] == "ローカライズされたトピック" && p[:excerpt] == "ローカライズされた投稿" }
.once
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: localizable_payload)
end
it "localizes payload before delivering to hub push" do
SiteSetting.allowed_user_api_push_urls = "https://hub.example.com/push"
client = Fabricate(:user_api_key_client)
Fabricate(
:user_api_key,
user: user,
scopes: ["notifications"].map { |name| UserApiKeyScope.new(name: name) },
push_url: "https://hub.example.com/push",
user_api_key_client_id: client.id,
)
Fabricate(
:topic_localization,
topic: topic,
locale: "ja",
title: "ローカライズされたトピック",
fancy_title: "ローカライズされたトピック",
)
body = nil
stub_request(:post, "https://hub.example.com/push").to_return do |request|
body = JSON.parse(request.body)
{ status: 200 }
end
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: localizable_payload)
expect(body["notifications"].first["topic_title"]).to eq("ローカライズされたトピック")
end
it "does not localize when content_localization_enabled is false" do
SiteSetting.content_localization_enabled = false
Fabricate(:push_subscription, user: user)
Fabricate(
:topic_localization,
topic: topic,
locale: "ja",
title: "ローカライズされたトピック",
fancy_title: "ローカライズされたトピック",
)
PushNotificationPusher
.expects(:push)
.with do |u, p|
p[:topic_title] == "Original topic title" || p["topic_title"] == "Original topic title"
end
.once
Jobs::DeliverPushNotification.new.execute(user_id: user.id, payload: localizable_payload)
end
end
end