# frozen_string_literal: true
require "theme_store/zip_exporter"
RSpec.describe ThemeStore::ZipExporter do
let(:rand_hex) { +"X" << SecureRandom.hex }
let!(:theme) do
Fabricate(:theme, name: "Header Icons").tap do |theme|
theme.set_field(target: :common, name: :body_tag, value: "testtheme1")
theme.set_field(target: :settings, name: :yaml, value: "somesetting: #{rand_hex}")
theme.set_field(
target: :mobile,
name: :scss,
value: "body {background-color: $background_color; font-size: $font-size}",
)
theme.set_field(
target: :translations,
name: :en,
value: { en: { key: "value" } }.deep_stringify_keys.to_yaml,
)
image = file_from_fixtures("logo.png")
upload = UploadCreator.new(image, "logo.png").create_for(Discourse::SYSTEM_USER_ID)
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
image = file_from_fixtures("logo.png")
_other_upload = UploadCreator.new(image, "logo.png").create_for(Discourse::SYSTEM_USER_ID)
theme.set_field(
target: :common,
name: "other_logo",
upload_id: upload.id,
type: :theme_upload_var,
)
theme.set_field(target: :migrations, name: "0201-some-migration", type: :js, value: <<~JS)
export default function migrate(settings) {
settings.set("aa", 1);
return settings;
}
JS
theme.build_remote_theme(
remote_url: "",
about_url: "abouturl",
license_url: "licenseurl",
authors: "David Taylor",
theme_version: "1.0",
minimum_discourse_version: "1.0.0",
maximum_discourse_version: "3.0.0.beta1",
)
cs1 =
Fabricate(
:color_scheme,
name: "Orphan Color Scheme",
color_scheme_colors: [
Fabricate(:color_scheme_color, name: "header_primary", hex: "F0F0F0"),
Fabricate(:color_scheme_color, name: "header_background", hex: "1E1E1E"),
Fabricate(:color_scheme_color, name: "tertiary", hex: "858585"),
],
)
cs2 =
Fabricate(
:color_scheme,
name: "Theme Color Scheme",
color_scheme_colors: [
Fabricate(:color_scheme_color, name: "header_primary", hex: "F0F0F0"),
Fabricate(:color_scheme_color, name: "header_background", hex: "1E1E1E"),
Fabricate(:color_scheme_color, name: "tertiary", hex: "858585"),
],
)
theme.color_scheme = cs1
cs2.update(theme_id: theme.id)
theme.save!
end
end
let(:dir) do
tmpdir = Dir.tmpdir
dir = "#{tmpdir}/#{SecureRandom.hex}"
FileUtils.mkdir(dir)
dir
end
after { FileUtils.rm_rf(dir) }
let(:package) do
exporter = ThemeStore::ZipExporter.new(theme)
filename = exporter.package_filename
FileUtils.cp(filename, dir)
exporter.cleanup!
"#{dir}/discourse-header-icons.zip"
end
it "exports the theme correctly" do
package
file = "discourse-header-icons.zip"
Dir.chdir(dir) do
available_size = SiteSetting.decompressed_theme_max_file_size_mb
Compression::Zip.new.decompress(dir, file, available_size)
`rm #{file}`
folders = Dir.glob("**/*").reject { |f| File.file?(f) }
expect(folders).to contain_exactly(
"assets",
"common",
"locales",
"mobile",
"migrations",
"migrations/settings",
)
files = Dir.glob("**/*").reject { |f| File.directory?(f) }
expect(files).to contain_exactly(
"about.json",
"assets/logo.png",
"assets/other_logo.png",
"common/body_tag.html",
"locales/en.yml",
"mobile/mobile.scss",
"settings.yml",
"migrations/settings/0201-some-migration.js",
)
expect(JSON.parse(File.read("about.json")).deep_symbolize_keys).to eq(
name: "Header Icons",
about_url: "abouturl",
license_url: "licenseurl",
component: false,
assets: {
logo: "assets/logo.png",
other_logo: "assets/other_logo.png",
},
authors: "David Taylor",
minimum_discourse_version: "1.0.0",
maximum_discourse_version: "3.0.0.beta1",
theme_version: "1.0",
color_schemes: {
"Orphan Color Scheme": {
header_primary: "F0F0F0",
header_background: "1E1E1E",
tertiary: "858585",
},
"Theme Color Scheme": {
header_primary: "F0F0F0",
header_background: "1E1E1E",
tertiary: "858585",
},
},
modifiers: {
},
learn_more: "https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966",
)
expect(File.read("common/body_tag.html")).to eq("testtheme1")
expect(File.read("mobile/mobile.scss")).to eq(
"body {background-color: $background_color; font-size: $font-size}",
)
expect(File.read("settings.yml")).to eq("somesetting: #{rand_hex}")
expect(File.read("locales/en.yml")).to eq(
{ en: { key: "value" } }.deep_stringify_keys.to_yaml,
)
expect(File.read("migrations/settings/0201-some-migration.js")).to eq(<<~JS)
export default function migrate(settings) {
settings.set("aa", 1);
return settings;
}
JS
theme.update!(name: "Discourse Header Icons")
exporter = ThemeStore::ZipExporter.new(theme)
filename = exporter.package_filename
exporter.cleanup!
expect(filename).to end_with "/discourse-header-icons.zip"
end
end
it "has safeguards to prevent writing outside the temp directory" do
# Theme field names should be sanitized before writing to the database,
# but protection is in place 'just in case'
expect do
theme.set_field(target: :translations, name: SiteSetting.default_locale, value: "hacked")
ThemeField.any_instance.stubs(:file_path).returns("../../malicious")
theme.save!
package
end.to raise_error(RuntimeError)
end
end