discourse/spec/support/helpers.rb
Osama Sayegh 3cadd6769e
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.

Example use cases for the theme settings migration system:

1. Renaming a theme setting.

2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).

3. Altering the format of data stored in a theme setting.

All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.

Usage:

1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.

2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.

3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).

4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.

5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.

6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.

Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:

```js
// File name: 0001-rename-setting.js

export default function migrate(settings) {
  if (settings.has("setting_with_old_name")) {
    settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
  }
  return settings;
}
```

Internal topic: t/109980
2023-11-02 08:10:15 +03:00

234 lines
6.7 KiB
Ruby

# frozen_string_literal: true
GIT_INITIAL_BRANCH_SUPPORTED =
Gem::Version.new(`git --version`.match(/[\d\.]+/)[0]) >= Gem::Version.new("2.28.0")
module Helpers
extend ActiveSupport::Concern
def self.next_seq
@next_seq = (@next_seq || 0) + 1
end
def log_in(fabricator = nil)
user = Fabricate(fabricator || :user)
log_in_user(user)
user
end
def log_in_user(user)
cookie_jar = ActionDispatch::Request.new(request.env).cookie_jar
provider = Discourse.current_user_provider.new(request.env)
provider.log_on_user(user, session, cookie_jar)
provider
end
def log_out_user(provider)
provider.log_off_user(session, cookies)
end
def fixture_file(filename)
return "" if filename.blank?
file_path = File.expand_path(Rails.root + "spec/fixtures/" + filename)
File.read(file_path)
end
def build(*args)
Fabricate.build(*args)
end
def create_topic(args = {})
args[:title] ||= "This is my title #{Helpers.next_seq}"
user = args.delete(:user) || Fabricate(:user)
guardian = Guardian.new(user)
args[:category] = args[:category].id if args[:category].is_a?(Category)
TopicCreator.create(user, guardian, args)
end
def create_post(args = {})
# Pretty much all the tests with `create_post` will fail without this
# since allow_uncategorized_topics is now false by default
SiteSetting.allow_uncategorized_topics = true unless args[:allow_uncategorized_topics] == false
args[:title] ||= "This is my title #{Helpers.next_seq}"
args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
args[:topic_id] = args[:topic].id if args[:topic]
automated_group_refresh_required = args[:user].blank?
user = args.delete(:user) || Fabricate(:user)
Group.refresh_automatic_groups! if automated_group_refresh_required
args[:category] = args[:category].id if args[:category].is_a?(Category)
creator = PostCreator.new(user, args)
post = creator.create
raise StandardError.new(creator.errors.full_messages.join(" ")) if creator.errors.present?
post
end
def stub_guardian(user)
guardian = Guardian.new(user)
yield(guardian) if block_given?
Guardian.stubs(new: guardian).with(user, anything)
end
def wait_for(on_fail: nil, timeout: 1, &blk)
i = 0
result = false
while !result && i < timeout * 1000
result = blk.call
i += 1
sleep 0.001
end
on_fail&.call
expect(result).to eq(true)
end
def email(email_name)
fixture_file("emails/#{email_name}.eml")
end
def create_staff_only_tags(tag_names)
create_limited_tags("Staff Tags", Group::AUTO_GROUPS[:staff], tag_names)
end
def create_limited_tags(tag_group_name, group_id, tag_names)
tag_group = Fabricate(:tag_group, name: tag_group_name)
TagGroupPermission.where(
tag_group: tag_group,
group_id: Group::AUTO_GROUPS[:everyone],
permission_type: TagGroupPermission.permission_types[:full],
).update(permission_type: TagGroupPermission.permission_types[:readonly])
TagGroupPermission.create!(
tag_group: tag_group,
group_id: group_id,
permission_type: TagGroupPermission.permission_types[:full],
)
tag_names.each do |name|
tag_group.tags << (Tag.where(name: name).first || Fabricate(:tag, name: name))
end
end
def create_hidden_tags(tag_names)
tag_group = Fabricate(:tag_group, name: "Hidden Tags", permissions: { staff: :full })
tag_names.each do |name|
tag_group.tags << (Tag.where(name: name).first || Fabricate(:tag, name: name))
end
end
def sorted_tag_names(tag_records)
tag_records.map { |t| t.is_a?(String) ? t : t.name }.sort
end
def expect_same_tag_names(a, b)
expect(sorted_tag_names(a)).to eq(sorted_tag_names(b))
end
def capture_output(output_name)
if ENV["RAILS_ENABLE_TEST_STDOUT"]
yield
return
end
previous_output = output_name == :stdout ? $stdout : $stderr
io = StringIO.new
output_name == :stdout ? $stdout = io : $stderr = io
yield
io.string
ensure
output_name == :stdout ? $stdout = previous_output : $stderr = previous_output
end
def capture_stdout(&block)
capture_output(:stdout, &block)
end
def capture_stderr(&block)
capture_output(:stderr, &block)
end
def set_subfolder(new_root)
global_setting :relative_url_root, new_root
old_root = ActionController::Base.config.relative_url_root
ActionController::Base.config.relative_url_root = new_root
Rails.application.routes.stubs(:relative_url_root).returns(new_root)
before_next_spec { ActionController::Base.config.relative_url_root = old_root }
if RSpec.current_example.metadata[:type] == :system
Capybara.app.map("/") { run lambda { |env| [404, {}, [""]] } }
Capybara.app.map(new_root) { run Rails.application }
before_next_spec do
Capybara.app.map(new_root) { run lambda { |env| [404, {}, [""]] } }
Capybara.app.map("/") { run Rails.application }
end
end
end
def setup_git_repo(files)
repo_dir = Dir.mktmpdir
`cd #{repo_dir} && git init . #{"--initial-branch=main" if GIT_INITIAL_BRANCH_SUPPORTED}`
`cd #{repo_dir} && git config user.email 'someone@cool.com'`
`cd #{repo_dir} && git config user.name 'The Cool One'`
`cd #{repo_dir} && git config commit.gpgsign 'false'`
files.each do |name, data|
FileUtils.mkdir_p(Pathname.new("#{repo_dir}/#{name}").dirname)
File.write("#{repo_dir}/#{name}", data)
`cd #{repo_dir} && git add #{name}`
end
`cd #{repo_dir} && git commit -am 'first commit'`
repo_dir
end
def add_to_git_repo(repo_dir, files)
files.each do |name, data|
FileUtils.mkdir_p(Pathname.new("#{repo_dir}/#{name}").dirname)
File.write("#{repo_dir}/#{name}", data)
`cd #{repo_dir} && git add #{name}`
end
`cd #{repo_dir} && git commit -am 'add #{files.size} files'`
repo_dir
end
def stub_const(target, const, value)
old = target.const_get(const)
target.send(:remove_const, const)
target.const_set(const, value)
yield
ensure
target.send(:remove_const, const)
target.const_set(const, old)
end
def track_sql_queries
queries = []
callback = ->(*, payload) do
queries << payload.fetch(:sql) unless %w[CACHE SCHEMA].include?(payload.fetch(:name))
end
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") { yield }
queries
end
def stub_ip_lookup(stub_addr, ips)
Addrinfo
.stubs(:getaddrinfo)
.with { |addr, _| addr == stub_addr }
.returns(
ips.map { |ip| Addrinfo.new([IPAddr.new(ip).ipv6? ? "AF_INET6" : "AF_INET", 80, nil, ip]) },
)
end
def with_search_indexer_enabled
SearchIndexer.enable
yield
ensure
SearchIndexer.disable
end
end