0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/services/admin_dashboard_section_loader_spec.rb
Krzysztof Kotlarek 87247653e2
FIX: Don't fail the dashboard when one section errors (#41720)
Previously, an exception in a single section's loader failed the entire
admin dashboard request, so one broken section left admins with no
dashboard at all.

This change rescues and logs per-section build errors in
`AdminDashboardSectionLoader` and returns `error: true` for the affected
section, so the rest of the dashboard renders normally while the failed
section shows an inline "couldn't load" message.

Dashboard when the whole endpoint is 404
<img width="1388" height="1307" alt="Screenshot 2026-07-15 at 10 17
25 am"
src="https://github.com/user-attachments/assets/a6a0c0ea-53eb-402f-b940-ecf4015e1757"
/>

Dashboard when 2 sections are broken
<img width="1456" height="1489" alt="Screenshot 2026-07-15 at 10 43
14 am"
src="https://github.com/user-attachments/assets/46e11855-bcc3-41b1-ad45-aa3a80dd367a"
/>
2026-07-15 17:20:51 +08:00

161 lines
5 KiB
Ruby
Vendored

# frozen_string_literal: true
describe AdminDashboardSectionLoader do
fab!(:admin)
after do
if thread_pool = described_class.instance_variable_get(:@thread_pool)
thread_pool.shutdown
thread_pool.wait_for_termination(timeout: 1)
described_class.remove_instance_variable(:@thread_pool)
end
end
describe ".build" do
it "ensures the sections are built in order with current user and dates" do
AdminDashboardSiteTraffic
.expects(:build)
.with do |kwargs|
kwargs[:start_date] == "2026-05-01" && kwargs[:end_date] == "2026-05-07" &&
kwargs[:guardian].is_a?(Guardian) && kwargs[:guardian].user.id == admin.id
end
.returns({ value: "traffic" })
AdminDashboardEngagement
.expects(:build)
.with(start_date: "2026-05-01", end_date: "2026-05-07", current_user: admin)
.returns({ value: "engagement" })
AdminDashboardSearch
.expects(:build)
.with(start_date: "2026-05-01", end_date: "2026-05-07")
.returns({ value: "search" })
expect(
described_class.build(
section_ids: %w[traffic engagement search],
current_user: admin,
start_date: "2026-05-01",
end_date: "2026-05-07",
),
).to eq(
[
{ id: "traffic", data: { value: "traffic" } },
{ id: "engagement", data: { value: "engagement" } },
{ id: "search", data: { value: "search" } },
],
)
end
it "returns partial section data when a section fails to build" do
error = StandardError.new("boom")
AdminDashboardSiteTraffic.stubs(:build).returns({ value: "traffic" })
AdminDashboardSearch.stubs(:build).raises(error)
Discourse.expects(:warn_exception).with(
error,
message: "Failed to build admin dashboard section",
env: {
section_id: "search",
},
)
expect(
described_class.build(
section_ids: %w[traffic search],
current_user: admin,
start_date: "2026-05-01",
end_date: "2026-05-07",
),
).to eq(
[{ id: "traffic", data: { value: "traffic" } }, { id: "search", data: nil, error: true }],
)
end
end
describe "plugin sections" do
it "routes a registered plugin section id to its loader block" do
loader = ->(start_date:, end_date:, current_user:) do
{ value: "support", user: current_user.id }
end
DiscoursePluginRegistry.stubs(:admin_dashboard_sections).returns(
[{ id: "support", enabled: -> { true }, loader: loader }],
)
expect(
described_class.build(
section_ids: ["support"],
current_user: admin,
start_date: "2026-05-01",
end_date: "2026-05-07",
),
).to eq([{ id: "support", data: { value: "support", user: admin.id } }])
end
it "returns nil data for an unknown section id" do
DiscoursePluginRegistry.stubs(:admin_dashboard_sections).returns([])
expect(
described_class.build(
section_ids: ["frobnitz"],
current_user: admin,
start_date: "2026-05-01",
end_date: "2026-05-07",
),
).to eq([{ id: "frobnitz", data: nil }])
end
end
describe ".pool_size" do
it "caps at the DB connection pool, reserving one for the request thread" do
ActiveRecord::Base.connection_pool.stubs(:size).returns(3)
expect(described_class.pool_size).to eq(2)
end
it "uses the desired count when the pool has room to spare" do
ActiveRecord::Base.connection_pool.stubs(:size).returns(100)
desired =
AdminDashboardSectionConfiguration::KNOWN_SECTIONS.size +
DiscoursePluginRegistry.admin_dashboard_sections.size
expect(described_class.pool_size).to eq(desired)
end
it "never drops below one" do
ActiveRecord::Base.connection_pool.stubs(:size).returns(1)
expect(described_class.pool_size).to eq(1)
end
end
end
describe AdminDashboardSectionLoader do
self.use_transactional_tests = false
after do
if thread_pool = described_class.instance_variable_get(:@thread_pool)
thread_pool.shutdown
thread_pool.wait_for_termination(timeout: 1)
described_class.remove_instance_variable(:@thread_pool)
end
end
it "returns database connections to the pool once a section finishes building" do
worker_thread = nil
loader = ->(start_date:, end_date:, current_user:) do
worker_thread = Thread.current
ActiveRecord::Base.connection.execute("SELECT 1")
{ value: "leaky" }
end
DiscoursePluginRegistry.stubs(:admin_dashboard_sections).returns(
[{ id: "leaky", enabled: -> { true }, loader: loader }],
)
described_class.build(
section_ids: ["leaky"],
current_user: Discourse.system_user,
start_date: "2026-05-01",
end_date: "2026-05-07",
)
expect(worker_thread).not_to eq(Thread.current)
wait_for do
ActiveRecord::Base.connection_pool.connections.none? { |c| c.owner == worker_thread }
end
end
end