0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/services/site_setting/splash_screen_image_changed_spec.rb
Kris 72a51e8943
DEV: clean up splash SVG on upload, not when rendering (#37681)
This moves the splash screen SVG processing (SMIL and script stripping)
to upload rather than render for the `splash_screen_image` setting.

While testing I also found a case where SVG dimensions could make splash
images very tiny, so I strip out the dimensions (as long as a viewbox is
present) so the SVG can scale to fit the wrapper better.

---------

Co-authored-by: Régis Hanol <regis@hanol.fr>
Co-authored-by: Martin Brennan <martin@discourse.org>
2026-02-16 13:14:35 +10:00

172 lines
5.4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe SiteSetting::SplashScreenImageChanged do
describe ".call" do
subject(:result) { described_class.call(params:) }
fab!(:user)
def create_svg_upload(svg_content, filename: "splash.svg")
file = file_from_contents(svg_content, filename, "images")
UploadCreator.new(file, filename).create_for(user.id)
end
def write_upload_file(upload, content)
path = Discourse.store.path_for(upload)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, content)
end
let(:params) { { upload_id: upload.id } }
let(:upload) { create_svg_upload(svg_content) }
context "when upload does not exist" do
let(:upload) { Fabricate(:upload, extension: "svg") }
before { upload.destroy! }
it { is_expected.to fail_to_find_a_model(:upload) }
end
context "when upload has no valid SVG content" do
let(:upload) do
u = Fabricate(:upload, extension: "svg", original_filename: "empty.svg")
write_upload_file(u, "")
u
end
it { is_expected.to fail_to_find_a_model(:svg) }
end
context "when upload content raises when read" do
let(:upload) { Fabricate(:upload, extension: "svg") }
it { is_expected.to fail_to_find_a_model(:svg) }
end
context "when upload has valid SVG content" do
let(:svg_with_animate) { <<~SVG }
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<rect x="0" y="0" width="100" height="100" fill="white"/>
<animate attributeName="opacity" from="1" to="0" dur="1s"/>
</svg>
SVG
context "when cleaned SVG differs from upload content" do
let(:upload) do
u = Fabricate(:upload, extension: "svg", original_filename: "splash.svg")
write_upload_file(u, svg_with_animate)
u
end
it { is_expected.to run_successfully }
it "updates the upload with new sha1 and url" do
original_sha1 = upload.sha1
original_url = upload.url
result
upload.reload
expect(upload.sha1).not_to eq(original_sha1)
expect(upload.url).not_to eq(original_url)
end
it "cleans the SVG (removes animate and width/height when viewBox present)" do
result
upload.reload
doc = Nokogiri.XML(upload.content)
svg = doc.at_css("svg")
expect(upload.content).not_to include("animate")
expect(svg["width"]).to be_nil
expect(svg["height"]).to be_nil
end
it "clears the splash screen SVG cache" do
Discourse
.cache
.expects(:delete)
.with { |key| key.match?(/\Asplash_screen_svg_#{upload.id}_[a-f0-9]{40}\z/) }
result
end
end
context "when another upload with the same cleaned sha1 exists" do
let(:cleaned_svg_content) do
doc = Nokogiri.XML(svg_with_animate)
svg = doc.at_css("svg")
svg.xpath(
".//*[local-name()='animate' or local-name()='animateTransform' or local-name()='animateMotion' or local-name()='set']",
).each(&:remove)
svg.remove_attribute("width") if svg["viewBox"].present?
svg.remove_attribute("height") if svg["viewBox"].present?
svg.to_xml
end
let!(:existing_upload) do
u = Fabricate(:upload, extension: "svg", original_filename: "existing.svg")
write_upload_file(u, cleaned_svg_content)
path = Discourse.store.path_for(u)
u.update!(sha1: Upload.generate_digest(path))
u
end
let(:upload) do
u = Fabricate(:upload, extension: "svg", original_filename: "splash.svg")
write_upload_file(u, svg_with_animate)
u
end
it { is_expected.to run_successfully }
it "sets splash_screen_image to the existing upload" do
result
expect(SiteSetting.splash_screen_image).to eq(existing_upload)
end
it "does not update the current upload" do
original_sha1 = upload.sha1
result
expect(upload.reload.sha1).to eq(original_sha1)
end
end
context "when cleaned SVG equals upload content" do
let(:idempotent_svg) do
doc = Nokogiri.XML(<<~SVG)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<rect x="0" y="0" width="10" height="10"/>
</svg>
SVG
svg = doc.at_css("svg")
svg.remove_attribute("width") if svg["viewBox"].present?
svg.remove_attribute("height") if svg["viewBox"].present?
svg.to_xml
end
let(:upload) do
u = Fabricate(:upload, extension: "svg", original_filename: "minimal.svg")
write_upload_file(u, idempotent_svg)
u
end
it { is_expected.to run_successfully }
it "does not change the upload sha1 or url" do
original_sha1 = upload.sha1
original_url = upload.url
result
upload.reload
expect(upload.sha1).to eq(original_sha1)
expect(upload.url).to eq(original_url)
end
it "still clears the cache" do
allow(Discourse.cache).to receive(:delete)
result
expect(Discourse.cache).to have_received(:delete).with(
"splash_screen_svg_#{upload.id}_#{upload.sha1}",
)
end
end
end
end
end