Some checks failed
When an application has no icon attached, render a deterministic monogram SVG instead of the generic picture-frame placeholder. Initials are picked from capital letters in the name (ShelfLife -> SL); fall back to the first two letters when fewer than two capitals exist (Audiobookshelf -> AU). Background colour is hashed from the name for stable per-app identity across visits. Adds an optional second icon attachment, icon_dark, alongside the main icon. When present, render a <picture> with a prefers-color-scheme: dark source so the browser swaps automatically; when absent, the main icon is used in both modes. The SVG sanitization, content-type fix, and size/format validation now run over both attachments uniformly. Bumps to 0.14.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
1.7 KiB
Ruby
60 lines
1.7 KiB
Ruby
require "test_helper"
|
|
|
|
class ApplicationTest < ActiveSupport::TestCase
|
|
test "sanitizes an SVG icon uploaded via UploadedFile (regression for FileNotFoundError)" do
|
|
app = applications(:kavita_app)
|
|
|
|
svg = %(<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script><path d="M0 0"/></svg>)
|
|
tempfile = Tempfile.new(["icon", ".svg"]).tap do |t|
|
|
t.write(svg)
|
|
t.rewind
|
|
end
|
|
uploaded = ActionDispatch::Http::UploadedFile.new(
|
|
tempfile: tempfile,
|
|
filename: "icon.svg",
|
|
type: "image/svg+xml"
|
|
)
|
|
|
|
# Previously raised ActiveStorage::FileNotFoundError because the
|
|
# before_validation callback called icon.download before the blob was
|
|
# uploaded to disk.
|
|
assert_nothing_raised do
|
|
app.update!(icon: uploaded)
|
|
end
|
|
|
|
cleaned = app.icon.download
|
|
refute_match(/<script/i, cleaned)
|
|
assert_match(/<path/, cleaned)
|
|
ensure
|
|
tempfile&.close
|
|
tempfile&.unlink
|
|
end
|
|
|
|
test "icon_dark is independently attachable and SVG-sanitized" do
|
|
app = applications(:kavita_app)
|
|
|
|
svg = %(<svg xmlns="http://www.w3.org/2000/svg"><script>boom()</script><circle cx="5" cy="5" r="3"/></svg>)
|
|
tempfile = Tempfile.new(["dark", ".svg"]).tap do |t|
|
|
t.write(svg)
|
|
t.rewind
|
|
end
|
|
uploaded = ActionDispatch::Http::UploadedFile.new(
|
|
tempfile: tempfile,
|
|
filename: "dark.svg",
|
|
type: "image/svg+xml"
|
|
)
|
|
|
|
assert_nothing_raised do
|
|
app.update!(icon_dark: uploaded)
|
|
end
|
|
|
|
assert app.icon_dark.attached?
|
|
cleaned = app.icon_dark.download
|
|
refute_match(/<script/i, cleaned)
|
|
assert_match(/<circle/, cleaned)
|
|
ensure
|
|
tempfile&.close
|
|
tempfile&.unlink
|
|
end
|
|
end
|