mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 06:15:28 +08:00
6e161d3e75
The most common thing that we do with fab! is: fab!(:thing) { Fabricate(:thing) } This commit adds a shorthand for this which is just simply: fab!(:thing) i.e. If you omit the block, then, by default, you'll get a `Fabricate`d object using the fabricator of the same name.
1094 lines
34 KiB
Ruby
1094 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Admin::ThemesController do
|
|
fab!(:admin)
|
|
fab!(:moderator)
|
|
fab!(:user)
|
|
|
|
let! :repo do
|
|
setup_git_repo("about.json" => { name: "discourse-branch-header" }.to_json)
|
|
end
|
|
|
|
let! :repo_url do
|
|
MockGitImporter.register("https://github.com/discourse/discourse-brand-header.git", repo)
|
|
end
|
|
|
|
around(:each) { |group| MockGitImporter.with_mock { group.run } }
|
|
|
|
describe "#generate_key_pair" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "can generate key pairs" do
|
|
post "/admin/themes/generate_key_pair.json"
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
expect(json["private_key"]).to eq(nil)
|
|
expect(json["public_key"]).to include("ssh-rsa ")
|
|
expect(Discourse.redis.get("ssh_key_#{json["public_key"]}")).not_to eq(nil)
|
|
end
|
|
end
|
|
|
|
shared_examples "key pair generation not allowed" do
|
|
it "prevents key pair generation with a 404 response" do
|
|
post "/admin/themes/generate_key_pair.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key pair generation not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key pair generation not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#upload_asset" do
|
|
let(:file) { file_from_fixtures("fake.woff2", "woff2") }
|
|
let(:filename) { File.basename(file) }
|
|
let(:upload) { Rack::Test::UploadedFile.new(file) }
|
|
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "can create a theme upload" do
|
|
post "/admin/themes/upload_asset.json", params: { file: upload }
|
|
expect(response.status).to eq(201)
|
|
|
|
upload = Upload.find_by(original_filename: filename)
|
|
|
|
expect(upload.id).not_to be_nil
|
|
expect(response.parsed_body["upload_id"]).to eq(upload.id)
|
|
end
|
|
|
|
context "when trying to upload an existing file" do
|
|
let(:uploaded_file) { Upload.find_by(original_filename: filename) }
|
|
let(:response_json) { response.parsed_body }
|
|
|
|
before do
|
|
post "/admin/themes/upload_asset.json", params: { file: upload }
|
|
expect(response.status).to eq(201)
|
|
end
|
|
|
|
it "reuses the original upload" do
|
|
expect(response.status).to eq(201)
|
|
expect(response_json["upload_id"]).to eq(uploaded_file.id)
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples "theme asset upload not allowed" do
|
|
it "prevents theme asset upload with a 404 response" do
|
|
expect do
|
|
post "/admin/themes/upload_asset.json", params: { file: upload }
|
|
end.not_to change { Upload.count }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme asset upload not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme asset upload not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#export" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "exports correctly" do
|
|
theme = Fabricate(:theme, name: "Awesome Theme")
|
|
theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
|
|
theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>")
|
|
theme.set_field(
|
|
target: :extra_js,
|
|
name: "discourse/controller/blah",
|
|
value: 'console.log("test");',
|
|
)
|
|
theme.save!
|
|
|
|
get "/admin/customize/themes/#{theme.id}/export"
|
|
expect(response.status).to eq(200)
|
|
|
|
# Save the output in a temp file (automatically cleaned up)
|
|
file = Tempfile.new("archive.zip")
|
|
file.write(response.body)
|
|
file.rewind
|
|
uploaded_file = Rack::Test::UploadedFile.new(file.path, "application/zip")
|
|
|
|
# Now import it again
|
|
expect do
|
|
post "/admin/themes/import.json", params: { theme: uploaded_file }
|
|
expect(response.status).to eq(201)
|
|
end.to change { Theme.count }.by (1)
|
|
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["name"]).to eq("Awesome Theme")
|
|
expect(json["theme"]["theme_fields"].length).to eq(3)
|
|
end
|
|
end
|
|
|
|
shared_examples "theme export not allowed" do
|
|
it "prevents theme export with a 404 response" do
|
|
theme = Fabricate(:theme, name: "Awesome Theme")
|
|
|
|
get "/admin/customize/themes/#{theme.id}/export"
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme export not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme export not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#import" do
|
|
let(:theme_json_file) do
|
|
Rack::Test::UploadedFile.new(
|
|
file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json"),
|
|
"application/json",
|
|
)
|
|
end
|
|
|
|
let(:theme_archive) do
|
|
Rack::Test::UploadedFile.new(
|
|
file_from_fixtures("discourse-test-theme.zip", "themes"),
|
|
"application/zip",
|
|
)
|
|
end
|
|
|
|
let(:image) { file_from_fixtures("logo.png") }
|
|
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
context "when theme allowlist mode is enabled" do
|
|
before do
|
|
global_setting :allowed_theme_repos,
|
|
"https://github.com/discourse/discourse-brand-header.git"
|
|
end
|
|
|
|
it "allows allowlisted imports" do
|
|
expect(Theme.allowed_remote_theme_ids.length).to eq(0)
|
|
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
remote: " https://github.com/discourse/discourse-brand-header.git ",
|
|
}
|
|
|
|
expect(Theme.allowed_remote_theme_ids.length).to eq(1)
|
|
expect(response.status).to eq(201)
|
|
end
|
|
|
|
it "prevents adding disallowed themes" do
|
|
RemoteTheme.stubs(:import_theme)
|
|
remote = " https://bad.com/discourse/discourse-brand-header.git "
|
|
|
|
post "/admin/themes/import.json", params: { remote: remote }
|
|
|
|
expect(response.status).to eq(403)
|
|
expect(response.parsed_body["errors"]).to include(
|
|
I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip }),
|
|
)
|
|
end
|
|
|
|
it "bans json file import" do
|
|
post "/admin/themes/import.json", params: { theme: theme_json_file }
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
it "can import a theme from Git" do
|
|
RemoteTheme.stubs(:import_theme)
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
remote: " https://github.com/discourse/discourse-brand-header.git ",
|
|
}
|
|
|
|
expect(response.status).to eq(201)
|
|
end
|
|
|
|
it "responds with suitable error message when a migration fails" do
|
|
repo_path =
|
|
setup_git_repo(
|
|
"about.json" => { name: "test theme" }.to_json,
|
|
"settings.yaml" => "boolean_setting: true",
|
|
"migrations/settings/0001-some-migration.js" => <<~JS,
|
|
export default function migrate(settings) {
|
|
settings.set("unknown_setting", "dsad");
|
|
return settings;
|
|
}
|
|
JS
|
|
)
|
|
repo_url = MockGitImporter.register("https://example.com/initial_repo.git", repo_path)
|
|
|
|
post "/admin/themes/import.json", params: { remote: repo_url }
|
|
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"]).to contain_exactly(
|
|
I18n.t(
|
|
"themes.import_error.migrations.unknown_setting_returned_by_migration",
|
|
name: "0001-some-migration",
|
|
setting_name: "unknown_setting",
|
|
),
|
|
)
|
|
end
|
|
|
|
it "fails to import with a failing status" do
|
|
post "/admin/themes/import.json", params: { remote: "non-existent" }
|
|
|
|
expect(response.status).to eq(422)
|
|
end
|
|
|
|
it "fails to import with a failing status" do
|
|
post "/admin/themes/import.json", params: { remote: "https://#{"a" * 10_000}.com" }
|
|
|
|
expect(response.status).to eq(422)
|
|
end
|
|
|
|
it "can lookup a private key by public key" do
|
|
Discourse.redis.setex("ssh_key_abcdef", 1.hour, "rsa private key")
|
|
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
remote: " #{repo_url} ",
|
|
public_key: "abcdef",
|
|
}
|
|
|
|
expect(RemoteTheme.last.private_key).to eq("rsa private key")
|
|
|
|
expect(response.status).to eq(201)
|
|
end
|
|
|
|
it "imports a theme" do
|
|
post "/admin/themes/import.json", params: { theme: theme_json_file }
|
|
expect(response.status).to eq(201)
|
|
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["name"]).to eq("Sam's Simple Theme")
|
|
expect(json["theme"]["theme_fields"].length).to eq(2)
|
|
expect(json["theme"]["auto_update"]).to eq(false)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
|
|
it "can fail if theme is not accessible" do
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
remote: "git@github.com:discourse/discourse-inexistent-theme.git",
|
|
}
|
|
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"]).to contain_exactly(I18n.t("themes.import_error.git"))
|
|
end
|
|
|
|
it "can force install theme" do
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
remote: "git@github.com:discourse/discourse-inexistent-theme.git",
|
|
force: true,
|
|
}
|
|
|
|
expect(response.status).to eq(201)
|
|
expect(response.parsed_body["theme"]["name"]).to eq("discourse-inexistent-theme")
|
|
end
|
|
|
|
it "fails to import with an error if uploads are not allowed" do
|
|
SiteSetting.theme_authorized_extensions = "nothing"
|
|
|
|
expect do post "/admin/themes/import.json", params: { theme: theme_archive } end.to change {
|
|
Theme.count
|
|
}.by (0)
|
|
|
|
expect(response.status).to eq(422)
|
|
end
|
|
|
|
it "imports a theme from an archive" do
|
|
_existing_theme = Fabricate(:theme, name: "Header Icons")
|
|
|
|
expect do post "/admin/themes/import.json", params: { theme: theme_archive } end.to change {
|
|
Theme.count
|
|
}.by (1)
|
|
expect(response.status).to eq(201)
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["name"]).to eq("Header Icons")
|
|
expect(json["theme"]["theme_fields"].length).to eq(5)
|
|
expect(json["theme"]["auto_update"]).to eq(false)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
|
|
it "updates an existing theme from an archive by id" do
|
|
# Used by theme CLI
|
|
_existing_theme = Fabricate(:theme, name: "Header Icons")
|
|
other_existing_theme = Fabricate(:theme, name: "Some other name")
|
|
|
|
messages =
|
|
MessageBus.track_publish do
|
|
expect do
|
|
post "/admin/themes/import.json",
|
|
params: {
|
|
bundle: theme_archive,
|
|
theme_id: other_existing_theme.id,
|
|
}
|
|
end.to change { Theme.count }.by (0)
|
|
end
|
|
expect(response.status).to eq(201)
|
|
json = response.parsed_body
|
|
|
|
# Ensure only one refresh message is sent.
|
|
# More than 1 is wasteful, and can trigger unusual race conditions in the client
|
|
# If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations
|
|
file_change_messages = messages.filter { |m| m[:channel] == "/file-change" }
|
|
expect(file_change_messages.count).to eq(1)
|
|
|
|
expect(json["theme"]["name"]).to eq("Some other name")
|
|
expect(json["theme"]["id"]).to eq(other_existing_theme.id)
|
|
expect(json["theme"]["theme_fields"].length).to eq(5)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
|
|
it "creates a new theme when id specified as nil" do
|
|
# Used by theme CLI
|
|
existing_theme = Fabricate(:theme, name: "Header Icons")
|
|
|
|
expect do
|
|
post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: nil }
|
|
end.to change { Theme.count }.by (1)
|
|
expect(response.status).to eq(201)
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["name"]).to eq("Header Icons")
|
|
expect(json["theme"]["id"]).not_to eq(existing_theme.id)
|
|
expect(json["theme"]["theme_fields"].length).to eq(5)
|
|
expect(json["theme"]["auto_update"]).to eq(false)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
end
|
|
|
|
shared_examples "theme import not allowed" do
|
|
it "prevents theme import with a 404 response" do
|
|
post "/admin/themes/import.json", params: { theme: theme_json_file }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme import not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme import not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#index" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "correctly returns themes" do
|
|
ColorScheme.destroy_all
|
|
Theme.destroy_all
|
|
|
|
theme = Fabricate(:theme)
|
|
theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
|
|
theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>")
|
|
|
|
theme.remote_theme =
|
|
RemoteTheme.new(
|
|
remote_url: "awesome.git",
|
|
remote_version: "7",
|
|
local_version: "8",
|
|
remote_updated_at: Time.zone.now,
|
|
)
|
|
|
|
theme.save!
|
|
|
|
# this will get serialized as well
|
|
ColorScheme.create_from_base(name: "test", colors: [])
|
|
|
|
get "/admin/themes.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
json = response.parsed_body
|
|
|
|
expect(json["extras"]["color_schemes"].length).to eq(1)
|
|
theme_json = json["themes"].find { |t| t["id"] == theme.id }
|
|
expect(theme_json["theme_fields"].length).to eq(2)
|
|
expect(theme_json["remote_theme"]["remote_version"]).to eq("7")
|
|
end
|
|
end
|
|
|
|
shared_examples "themes inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
get "/admin/themes.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "themes inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "themes inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#create" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "creates a theme" do
|
|
post "/admin/themes.json",
|
|
params: {
|
|
theme: {
|
|
name: "my test name",
|
|
theme_fields: [name: "scss", target: "common", value: "body{color: red;}"],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(201)
|
|
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["theme_fields"].length).to eq(1)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
end
|
|
|
|
shared_examples "theme creation not allowed" do
|
|
it "prevents creation with a 404 response" do
|
|
expect do
|
|
post "/admin/themes.json",
|
|
params: {
|
|
theme: {
|
|
name: "my test name",
|
|
theme_fields: [name: "scss", target: "common", value: "body{color: red;}"],
|
|
},
|
|
}
|
|
end.not_to change { Theme.count }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme creation not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme creation not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#update" do
|
|
let!(:theme) { Fabricate(:theme) }
|
|
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "returns the right response when an invalid id is given" do
|
|
put "/admin/themes/99999.json"
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
|
|
it "can change default theme" do
|
|
SiteSetting.default_theme_id = -1
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { id: theme.id, theme: { default: true } }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(SiteSetting.default_theme_id).to eq(theme.id)
|
|
end
|
|
|
|
it "can unset default theme" do
|
|
SiteSetting.default_theme_id = theme.id
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { theme: { default: false } }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(SiteSetting.default_theme_id).to eq(-1)
|
|
end
|
|
|
|
context "when theme allowlist mode is enabled" do
|
|
before do
|
|
global_setting :allowed_theme_repos, " https://magic.com/repo.git, https://x.com/git"
|
|
end
|
|
|
|
it "unconditionally bans theme_fields from updating" do
|
|
r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git")
|
|
theme.update!(remote_theme_id: r.id)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
name: "my test name",
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "scss", target: "desktop", value: "body{color: blue;}" },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
it "updates a theme" do
|
|
theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
|
|
theme.save
|
|
|
|
child_theme = Fabricate(:theme, component: true)
|
|
|
|
upload =
|
|
UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(
|
|
Discourse.system_user.id,
|
|
)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
child_theme_ids: [child_theme.id],
|
|
name: "my test name",
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "scss", target: "desktop", value: "body{color: blue;}" },
|
|
{ name: "bob", target: "common", value: "", type_id: 2, upload_id: upload.id },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
json = response.parsed_body
|
|
|
|
expect(json["theme"]["theme_fields"].length).to eq(2)
|
|
expect(json["theme"]["child_themes"].length).to eq(1)
|
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
|
end
|
|
|
|
it "prevents theme update when using ember css selectors" do
|
|
child_theme = Fabricate(:theme, component: true)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
child_theme_ids: [child_theme.id],
|
|
name: "my test name",
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "scss", target: "desktop", value: ".ember-view{color: blue;}" },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
json = response.parsed_body
|
|
|
|
fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] }
|
|
expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error"))
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
child_theme_ids: [child_theme.id],
|
|
name: "my test name",
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "scss", target: "desktop", value: "#ember392{color: blue;}" },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
|
|
fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] }
|
|
expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error"))
|
|
end
|
|
|
|
it "blocks remote theme fields from being locally edited" do
|
|
r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git")
|
|
theme.update!(remote_theme_id: r.id)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "header", target: "common", value: "filename.jpg", upload_id: 4 },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(403)
|
|
end
|
|
|
|
it "creates new theme fields" do
|
|
expect(theme.theme_fields.count).to eq(0)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
theme_fields: [{ name: "scss", target: "common", value: "test" }],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
theme.reload
|
|
expect(theme.theme_fields.count).to eq(1)
|
|
theme_field = theme.theme_fields.first
|
|
expect(theme_field.name).to eq("scss")
|
|
expect(theme_field.target_id).to eq(Theme.targets[:common])
|
|
expect(theme_field.value).to eq("test")
|
|
end
|
|
|
|
it "doesn't create theme fields when they don't pass validation" do
|
|
expect(theme.theme_fields.count).to eq(0)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "Na " * 1024**2 + "Batman!" },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(422)
|
|
json = JSON.parse(response.body)
|
|
expect(json["errors"].first).to include("Value is too long")
|
|
end
|
|
|
|
it "allows zip-imported theme fields to be locally edited" do
|
|
r = RemoteTheme.create!(remote_url: "")
|
|
theme.update!(remote_theme_id: r.id)
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
theme_fields: [
|
|
{ name: "scss", target: "common", value: "" },
|
|
{ name: "header", target: "common", value: "filename.jpg", upload_id: 4 },
|
|
],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "updates a child theme" do
|
|
child_theme = Fabricate(:theme, component: true)
|
|
put "/admin/themes/#{child_theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
parent_theme_ids: [theme.id],
|
|
},
|
|
}
|
|
expect(child_theme.parent_themes).to eq([theme])
|
|
end
|
|
|
|
it "can update translations" do
|
|
theme.set_field(
|
|
target: :translations,
|
|
name: :en,
|
|
value: { en: { somegroup: { somestring: "defaultstring" } } }.deep_stringify_keys.to_yaml,
|
|
)
|
|
theme.save!
|
|
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
translations: {
|
|
"somegroup.somestring" => "overriddenstring",
|
|
},
|
|
},
|
|
}
|
|
|
|
# Response correct
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
expect(json["theme"]["translations"][0]["value"]).to eq("overriddenstring")
|
|
|
|
# Database correct
|
|
theme.reload
|
|
expect(theme.theme_translation_overrides.count).to eq(1)
|
|
expect(theme.theme_translation_overrides.first.translation_key).to eq(
|
|
"somegroup.somestring",
|
|
)
|
|
|
|
# Set back to default
|
|
put "/admin/themes/#{theme.id}.json",
|
|
params: {
|
|
theme: {
|
|
translations: {
|
|
"somegroup.somestring" => "defaultstring",
|
|
},
|
|
},
|
|
}
|
|
# Response correct
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
expect(json["theme"]["translations"][0]["value"]).to eq("defaultstring")
|
|
|
|
# Database correct
|
|
theme.reload
|
|
expect(theme.theme_translation_overrides.count).to eq(0)
|
|
end
|
|
|
|
it "checking for updates saves the remote_theme record" do
|
|
theme.remote_theme =
|
|
RemoteTheme.create!(
|
|
remote_url: "http://discourse.org",
|
|
remote_version: "a",
|
|
local_version: "a",
|
|
commits_behind: 0,
|
|
)
|
|
theme.save!
|
|
ThemeStore::GitImporter.any_instance.stubs(:import!)
|
|
ThemeStore::GitImporter.any_instance.stubs(:commits_since).returns(["b", 1])
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { theme: { remote_check: true } }
|
|
theme.reload
|
|
expect(theme.remote_theme.remote_version).to eq("b")
|
|
expect(theme.remote_theme.commits_behind).to eq(1)
|
|
end
|
|
|
|
it "can disable component" do
|
|
child = Fabricate(:theme, component: true)
|
|
|
|
put "/admin/themes/#{child.id}.json", params: { theme: { enabled: false } }
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
expect(json["theme"]["enabled"]).to eq(false)
|
|
expect(
|
|
UserHistory.where(
|
|
context: child.id.to_s,
|
|
action: UserHistory.actions[:disable_theme_component],
|
|
).size,
|
|
).to eq(1)
|
|
expect(json["theme"]["disabled_by"]["id"]).to eq(admin.id)
|
|
end
|
|
|
|
it "enabling/disabling a component creates the correct staff action log" do
|
|
child = Fabricate(:theme, component: true)
|
|
UserHistory.destroy_all
|
|
|
|
put "/admin/themes/#{child.id}.json", params: { theme: { enabled: false } }
|
|
expect(response.status).to eq(200)
|
|
|
|
expect(
|
|
UserHistory.where(
|
|
context: child.id.to_s,
|
|
action: UserHistory.actions[:disable_theme_component],
|
|
).size,
|
|
).to eq(1)
|
|
expect(
|
|
UserHistory.where(
|
|
context: child.id.to_s,
|
|
action: UserHistory.actions[:enable_theme_component],
|
|
).size,
|
|
).to eq(0)
|
|
|
|
put "/admin/themes/#{child.id}.json", params: { theme: { enabled: true } }
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
|
|
expect(
|
|
UserHistory.where(
|
|
context: child.id.to_s,
|
|
action: UserHistory.actions[:disable_theme_component],
|
|
).size,
|
|
).to eq(1)
|
|
expect(
|
|
UserHistory.where(
|
|
context: child.id.to_s,
|
|
action: UserHistory.actions[:enable_theme_component],
|
|
).size,
|
|
).to eq(1)
|
|
|
|
expect(json["theme"]["disabled_by"]).to eq(nil)
|
|
expect(json["theme"]["enabled"]).to eq(true)
|
|
end
|
|
|
|
it "handles import errors on update" do
|
|
theme.create_remote_theme!(remote_url: "https://example.com/repository")
|
|
theme.save!
|
|
|
|
# RemoteTheme is extensively tested, and setting up the test scaffold is a large overhead
|
|
# So use a stub here to test the controller
|
|
RemoteTheme
|
|
.any_instance
|
|
.stubs(:update_from_remote)
|
|
.raises(RemoteTheme::ImportError.new("error message"))
|
|
put "/admin/themes/#{theme.id}.json", params: { theme: { remote_update: true } }
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"].first).to eq("error message")
|
|
end
|
|
|
|
it "returns the right error message" do
|
|
theme.update!(component: true)
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { theme: { default: true } }
|
|
|
|
expect(response.status).to eq(400)
|
|
expect(response.parsed_body["errors"].first).to include(
|
|
I18n.t("themes.errors.component_no_default"),
|
|
)
|
|
end
|
|
|
|
it "prevents converting the default theme to a component" do
|
|
SiteSetting.default_theme_id = theme.id
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { theme: { component: true } }
|
|
|
|
# should this error message be localized? InvalidParameters :component
|
|
expect(response.status).to eq(400)
|
|
expect(response.parsed_body["errors"].first).to include("component")
|
|
end
|
|
end
|
|
|
|
shared_examples "theme update not allowed" do
|
|
it "prevents updates with a 404 response" do
|
|
SiteSetting.default_theme_id = -1
|
|
|
|
put "/admin/themes/#{theme.id}.json", params: { id: theme.id, theme: { default: true } }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(SiteSetting.default_theme_id).not_to eq(theme.id)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme update not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme update not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#destroy" do
|
|
let!(:theme) { Fabricate(:theme) }
|
|
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "returns the right response when an invalid id is given" do
|
|
delete "/admin/themes/9999.json"
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
|
|
it "deletes the field's javascript cache" do
|
|
theme.set_field(
|
|
target: :common,
|
|
name: :header,
|
|
value: '<script>console.log("test")</script>',
|
|
)
|
|
theme.save!
|
|
|
|
javascript_cache =
|
|
theme
|
|
.theme_fields
|
|
.find_by(target_id: Theme.targets[:common], name: :header)
|
|
.javascript_cache
|
|
expect(javascript_cache).to_not eq(nil)
|
|
|
|
delete "/admin/themes/#{theme.id}.json"
|
|
|
|
expect(response.status).to eq(204)
|
|
expect { theme.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
expect { javascript_cache.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
end
|
|
|
|
shared_examples "theme deletion not allowed" do
|
|
it "prevent deletion with a 404 response" do
|
|
delete "/admin/themes/#{theme.id}.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(theme.reload).to be_present
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme deletion not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme deletion not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#preview" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "should return the right response when an invalid id is given" do
|
|
get "/admin/themes/9999/preview.json"
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
|
|
shared_examples "theme previews inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
theme = Fabricate(:theme)
|
|
|
|
get "/admin/themes/#{theme.id}/preview.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme previews inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme previews inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#update_single_setting" do
|
|
let(:theme) { Fabricate(:theme) }
|
|
|
|
before do
|
|
theme.set_field(target: :settings, name: :yaml, value: "bg: red")
|
|
theme.save!
|
|
end
|
|
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "should update a theme setting" do
|
|
put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg", value: "green" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["bg"]).to eq("green")
|
|
|
|
theme.reload
|
|
expect(theme.cached_settings[:bg]).to eq("green")
|
|
user_history = UserHistory.last
|
|
|
|
expect(user_history.action).to eq(UserHistory.actions[:change_theme_setting])
|
|
end
|
|
|
|
it "should clear a theme setting" do
|
|
put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg" }
|
|
theme.reload
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(theme.cached_settings[:bg]).to eq("")
|
|
end
|
|
end
|
|
|
|
shared_examples "theme update not allowed" do
|
|
it "prevents updates with a 404 response" do
|
|
put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg", value: "green" }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
|
|
theme.reload
|
|
expect(theme.cached_settings[:bg]).to eq("red")
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "theme update not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "theme update not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#bulk_destroy" do
|
|
fab!(:theme) { Fabricate(:theme, name: "Awesome Theme") }
|
|
fab!(:theme_2) { Fabricate(:theme, name: "Another awesome Theme") }
|
|
let(:theme_ids) { [theme.id, theme_2.id] }
|
|
|
|
before { sign_in(admin) }
|
|
|
|
it "destroys all selected the themes" do
|
|
expect do
|
|
delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
|
|
end.to change { Theme.count }.by(-2)
|
|
end
|
|
|
|
it "logs the theme destroy action for each theme" do
|
|
StaffActionLogger.any_instance.expects(:log_theme_destroy).twice
|
|
delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
|
|
end
|
|
end
|
|
end
|