e51f0bfee7
Account incineration could leave orphaned records due to missing cascade declarations and async jobs failing when the account was already gone. Cascade fixes: - Add Account::Searchable concern to clean up Search::Query (delete_all) and Search::Record (destroy_all, respects SQLite FTS dependent: :destroy) - Add before_destroy in Account::Storage to delete storage entries - Suppress storage entry recording during incineration so attachment purge callbacks don't create entries or enqueue materialize jobs - Guard Access#clean_inaccessible_data_later with unless user.destroyed? to avoid enqueuing pointless jobs during user cascade Job tenancy: - Extract AccountTenanted concern from the global ActiveJob initializer into ApplicationJob (include) with targeted prepends for ActionMailer::MailDeliveryJob and Turbo broadcast jobs - Defer account resolution from deserialize to perform so that missing accounts raise DeserializationError inside the execution path where discard_on can handle it Tests: - Comprehensive incineration test covering 35 model types with before/after assertions and full enqueued job processing - Mailer deliver_later test verifying account context survives job serialization for multi-account users - Turbo broadcast test verifying account-scoped URLs in rendered partials
127 lines
4.5 KiB
Ruby
127 lines
4.5 KiB
Ruby
require "test_helper"
|
|
|
|
class Notification::BundleMailerTest < ActionMailer::TestCase
|
|
setup do
|
|
@user = users(:david)
|
|
@user.notifications.destroy_all
|
|
|
|
@bundle = Notification::Bundle.create!(
|
|
user: @user,
|
|
starts_at: 1.hour.ago,
|
|
ends_at: 1.hour.from_now
|
|
)
|
|
end
|
|
|
|
test "renders avatar with initials in span when avatar is not attached" do
|
|
create_notification(@user)
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
|
|
avatar = html.at_css("span.avatar")
|
|
assert avatar, "Expected a span.avatar element"
|
|
assert_equal @user.initials, avatar.text.strip
|
|
assert_match /background-color: #[A-F0-9]{6}/, avatar["style"]
|
|
end
|
|
|
|
test "renders avatar with external image URL when avatar is attached" do
|
|
@user.avatar.attach(
|
|
io: File.open(Rails.root.join("test", "fixtures", "files", "avatar.png")),
|
|
filename: "avatar.png",
|
|
content_type: "image/png"
|
|
)
|
|
|
|
create_notification(@user)
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
|
|
avatar = html.at_css("img.avatar")
|
|
assert avatar, "Expected an img.avatar element"
|
|
assert avatar["src"].present?
|
|
assert_equal @user.name, avatar["alt"]
|
|
end
|
|
|
|
test "groups notifications by board, sorted alphabetically" do
|
|
private_board = boards(:private)
|
|
private_card = Current.with(user: @user) do
|
|
private_board.cards.create!(
|
|
title: "Private card", creator: @user, status: :published, account: @user.account
|
|
)
|
|
end
|
|
private_event = Event.create!(
|
|
creator: @user, board: private_board, eventable: private_card,
|
|
action: :card_published, account: @user.account
|
|
)
|
|
|
|
create_notification(@user, source: events(:logo_published))
|
|
create_notification(@user, source: private_event)
|
|
create_notification(@user, source: events(:layout_published))
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
|
|
board_headers = html.css(".notification__board")
|
|
assert_equal 2, board_headers.size, "Should have exactly two board headers"
|
|
assert_equal [ "Private board", "Writebook" ], board_headers.map(&:text)
|
|
end
|
|
|
|
test "board header links to the board" do
|
|
create_notification(@user, source: events(:logo_published))
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
board = boards(:writebook)
|
|
|
|
link = html.at_css(".notification__board a")
|
|
assert_equal "Writebook", link.text
|
|
assert_match %r{boards/#{board.id}}, link["href"]
|
|
end
|
|
|
|
test "shows multiple cards under same board header" do
|
|
create_notification(@user, source: events(:logo_published))
|
|
create_notification(@user, source: events(:layout_published))
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
|
|
assert_equal 1, html.css(".notification__board").size, "Same board should only have one header"
|
|
|
|
card_titles = html.css(".card__title").map(&:text)
|
|
assert_includes card_titles, "#1 The logo isn't big enough"
|
|
assert_includes card_titles, "#2 Layout is broken"
|
|
end
|
|
|
|
test "renders inline code in card title" do
|
|
cards(:logo).update_column :title, "Fix the `bug` in production"
|
|
create_notification(@user, source: events(:logo_published))
|
|
|
|
html = Nokogiri::HTML5(Notification::BundleMailer.notification(@bundle).html_part.body.to_s)
|
|
|
|
title_link = html.at_css(".card__title")
|
|
assert_equal "#1 Fix the <code>bug</code> in production", title_link.inner_html
|
|
end
|
|
|
|
test "skips notifications whose source event was deleted" do
|
|
notification = create_notification(@user)
|
|
notification.source.destroy
|
|
|
|
email = Notification::BundleMailer.notification(@bundle)
|
|
assert_not email.respond_to?(:deliver) && email.message.is_a?(Mail::Message),
|
|
"Should not generate a real email when all notifications are stale"
|
|
end
|
|
|
|
test "deliver_later works with account context" do
|
|
# Give user a second account so the mailer includes the account name in the subject
|
|
second_account = Account.create!(name: "Second account")
|
|
second_account.users.create!(identity: @user.identity, role: :member, name: @user.name)
|
|
|
|
create_notification(@user)
|
|
|
|
Notification::BundleMailer.notification(@bundle).deliver_later
|
|
perform_enqueued_jobs
|
|
|
|
assert_emails 1
|
|
end
|
|
|
|
private
|
|
def create_notification(user, source: events(:logo_published))
|
|
Notification.create!(user: user, creator: user, source: source, created_at: 30.minutes.ago)
|
|
end
|
|
end
|