mirror of
https://github.com/discourse/discourse.git
synced 2025-02-06 23:37:15 +08:00
b3f1192312
This commit adds a `getCategoryIdByName` helper function to the context in which theme migrations are ran in. The purpose of this commit is to allow themes which may have stored category names in theme settings to migrate to objects typed theme settings which supports properties of categories typed which stores the category ids in the value of the property.
305 lines
11 KiB
Ruby
305 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe ThemeSettingsMigrationsRunner do
|
|
fab!(:theme)
|
|
fab!(:migration_field) { Fabricate(:migration_theme_field, version: 1, theme: theme) }
|
|
fab!(:settings_field) { Fabricate(:settings_theme_field, theme: theme, value: <<~YAML) }
|
|
integer_setting: 1
|
|
boolean_setting: true
|
|
string_setting: ""
|
|
YAML
|
|
|
|
describe "#run" do
|
|
it "passes values of overridden settings only to migrations" do
|
|
theme.update_setting(:integer_setting, 1)
|
|
theme.update_setting(:string_setting, "osama")
|
|
theme.save!
|
|
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
if (settings.get("integer_setting") !== 1) {
|
|
throw new Error(`expected integer_setting to equal 1, but it's actually ${settings.get("integer_setting")}`);
|
|
}
|
|
if (settings.get("string_setting") !== "osama") {
|
|
throw new Error(`expected string_setting to equal "osama", but it's actually "${settings.get("string_setting")}"`);
|
|
}
|
|
if (settings.size !== 2) {
|
|
throw new Error(`expected the settings map to have only 2 keys, but instead got ${settings.size} keys`);
|
|
}
|
|
return settings;
|
|
}
|
|
JS
|
|
results = described_class.new(theme).run
|
|
expect(results.first[:theme_field_id]).to eq(migration_field.id)
|
|
expect(results.first[:settings_before]).to eq(
|
|
{ "integer_setting" => 1, "string_setting" => "osama" },
|
|
)
|
|
end
|
|
|
|
it "passes the output of the previous migration as input to the next one" do
|
|
theme.update_setting(:integer_setting, 1)
|
|
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
settings.set("integer_setting", 111);
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
another_migration_field =
|
|
Fabricate(:migration_theme_field, theme: theme, version: 2, value: <<~JS)
|
|
export default function migrate(settings) {
|
|
if (settings.get("integer_setting") !== 111) {
|
|
throw new Error(`expected integer_setting to equal 111, but it's actually ${settings.get("integer_setting")}`);
|
|
}
|
|
settings.set("integer_setting", 222);
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results.size).to eq(2)
|
|
|
|
expect(results[0][:theme_field_id]).to eq(migration_field.id)
|
|
expect(results[1][:theme_field_id]).to eq(another_migration_field.id)
|
|
|
|
expect(results[0][:settings_before]).to eq({})
|
|
expect(results[0][:settings_after]).to eq({ "integer_setting" => 111 })
|
|
|
|
expect(results[1][:settings_before]).to eq({ "integer_setting" => 111 })
|
|
expect(results[1][:settings_after]).to eq({ "integer_setting" => 222 })
|
|
end
|
|
|
|
it "doesn't run migrations that have already been ran" do
|
|
Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field)
|
|
|
|
pending_field = Fabricate(:migration_theme_field, theme: theme, version: 23)
|
|
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results.size).to eq(1)
|
|
|
|
expect(results.first[:version]).to eq(23)
|
|
expect(results.first[:theme_field_id]).to eq(pending_field.id)
|
|
end
|
|
|
|
it "doesn't error when no migrations have been ran yet" do
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results.size).to eq(1)
|
|
expect(results.first[:version]).to eq(1)
|
|
expect(results.first[:theme_field_id]).to eq(migration_field.id)
|
|
end
|
|
|
|
it "doesn't error when there are no pending migrations" do
|
|
Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field)
|
|
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results.size).to eq(0)
|
|
end
|
|
|
|
it "raises an error when there are too many pending migrations" do
|
|
Fabricate(:migration_theme_field, theme: theme, version: 2)
|
|
|
|
expect do described_class.new(theme, limit: 1).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.too_many_pending_migrations"),
|
|
)
|
|
end
|
|
|
|
it "raises an error if a migration field has a badly formatted name" do
|
|
migration_field.update_attribute(:name, "020-some-name")
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "020-some-name"),
|
|
)
|
|
|
|
migration_field.update_attribute(:name, "0020some-name")
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "0020some-name"),
|
|
)
|
|
|
|
migration_field.update_attribute(:name, "0020")
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "0020"),
|
|
)
|
|
end
|
|
|
|
it "raises an error if a pending migration has version lower than the last ran migration" do
|
|
migration_field.update!(name: "0020-some-name")
|
|
Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field, version: 20)
|
|
|
|
Fabricate(:migration_theme_field, theme: theme, version: 19, name: "0019-failing-migration")
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t(
|
|
"themes.import_error.migrations.out_of_sequence",
|
|
name: "0019-failing-migration",
|
|
current: 20,
|
|
),
|
|
)
|
|
end
|
|
|
|
it "detects bad syntax in migrations and raises an error" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate() {
|
|
JS
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t(
|
|
"themes.import_error.migrations.syntax_error",
|
|
name: "0001-some-name",
|
|
error:
|
|
'SyntaxError: "/discourse/theme/migration: Unexpected token (2:0)\n\n 1 | export default function migrate() {\n> 2 |\n | ^"',
|
|
),
|
|
)
|
|
end
|
|
|
|
it "imposes memory limit on migrations and raises an error if they exceed the limit" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
let a = new Array(10000);
|
|
while(true) {
|
|
a = a.concat(new Array(10000));
|
|
}
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
expect do described_class.new(theme, memory: 10.kilobytes).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.exceeded_memory_limit", name: "0001-some-name"),
|
|
)
|
|
end
|
|
|
|
it "imposes time limit on migrations and raises an error if they exceed the limit" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
let a = 1;
|
|
while(true) {
|
|
a += 1;
|
|
}
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
expect do described_class.new(theme, timeout: 10).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.timed_out", name: "0001-some-name"),
|
|
)
|
|
end
|
|
|
|
it "raises a clear error message when the migration file doesn't export anything" do
|
|
migration_field.update!(value: <<~JS)
|
|
function migrate(settings) {
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.no_exported_function", name: "0001-some-name"),
|
|
)
|
|
end
|
|
|
|
it "raises a clear error message when the migration file exports the default as something that's not a function" do
|
|
migration_field.update!(value: <<~JS)
|
|
export function migrate(settings) {
|
|
return settings;
|
|
}
|
|
|
|
const AA = 1;
|
|
export default AA;
|
|
JS
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t(
|
|
"themes.import_error.migrations.default_export_not_a_function",
|
|
name: "0001-some-name",
|
|
),
|
|
)
|
|
end
|
|
|
|
it "raises a clear error message when the migration function doesn't return anything" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {}
|
|
JS
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.no_returned_value", name: "0001-some-name"),
|
|
)
|
|
end
|
|
|
|
it "raises a clear error message when the migration function doesn't return a Map" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
return {};
|
|
}
|
|
JS
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t("themes.import_error.migrations.wrong_return_type", name: "0001-some-name"),
|
|
)
|
|
end
|
|
|
|
it "surfaces runtime errors that occur within the migration" do
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings) {
|
|
null.toString();
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
expect do described_class.new(theme).run end.to raise_error(
|
|
Theme::SettingsMigrationError,
|
|
I18n.t(
|
|
"themes.import_error.migrations.runtime_error",
|
|
name: "0001-some-name",
|
|
error: "TypeError: Cannot read properties of null (reading 'toString')",
|
|
),
|
|
)
|
|
end
|
|
|
|
it "returns a list of objects that each has data representing the migration and the results" do
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results[0][:version]).to eq(1)
|
|
expect(results[0][:name]).to eq("some-name")
|
|
expect(results[0][:original_name]).to eq("0001-some-name")
|
|
expect(results[0][:theme_field_id]).to eq(migration_field.id)
|
|
expect(results[0][:settings_before]).to eq({})
|
|
expect(results[0][:settings_after]).to eq({})
|
|
end
|
|
|
|
it "attaches the getCategoryIdByName() function to the context of the migrations" do
|
|
category = Fabricate(:category, name: "some-category")
|
|
|
|
theme.update_setting(:integer_setting, -10)
|
|
theme.save!
|
|
|
|
migration_field.update!(value: <<~JS)
|
|
export default function migrate(settings, helpers) {
|
|
const categoryId = helpers.getCategoryIdByName("some-category");
|
|
settings.set("integer_setting", categoryId);
|
|
return settings;
|
|
}
|
|
JS
|
|
|
|
results = described_class.new(theme).run
|
|
|
|
expect(results[0][:settings_after]).to eq({ "integer_setting" => category.id })
|
|
end
|
|
end
|
|
end
|