discourse/spec/support/helpers.rb
Alan Guo Xiang Tan 243fcb6ffc
DEV: Introduce run_theme_migration spec helper in test environment (#26845)
This commit introduces the `run_theme_migration` spec helper to allow
theme developers to write RSpec tests for theme migrations. For example,
this allows the following RSpec test to be written in themes:

```
RSpec.describe "0003-migrate-small-links-setting migration" do
  let!(:theme) { upload_theme_component }

  it "should set target property to `_blank` if previous target component is not valid or empty" do
    theme.theme_settings.create!(
      name: "small_links",
      theme: theme,
      data_type: ThemeSetting.types[:string],
      value: "some text, #|some text 2, #, invalid target",
    )

    run_theme_migration(theme, "0003-migrate-small-links-setting")

    expect(theme.settings[:small_links].value).to eq(
      [
        { "text" => "some text", "url" => "#", "target" => "_blank" },
        { "text" => "some text 2", "url" => "#", "target" => "_blank" },
      ],
    )
  end
end
```

This change is being introduced because we realised that writting just
javascript tests for the migrations is insufficient since javascript
tests do not ensure that the migrated theme settings can actually be
successfully saved into the database. Hence, we are introduce this
helper as a way for theme developers to write "end-to-end" migrations
tests.
2024-05-03 06:29:18 +08:00

299 lines
8.9 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)
user = Fabricate(:user, refresh_auto_groups: true) if !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]
user = args.delete(:user) || Fabricate(:user, refresh_auto_groups: true)
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
# Uploads a theme from a directory.
#
# @param set_theme_as_default [Boolean] Whether to set the uploaded theme as the default theme for the site. Defaults to true.
#
# @return [Theme] The uploaded theme model given by `models/theme.rb`.
#
# @example Upload a theme and set it as default
# upload_theme("/path/to/theme")
def upload_theme(set_theme_as_default: true)
theme = RemoteTheme.import_theme_from_directory(theme_dir_from_caller)
if theme.component
raise "Uploaded theme is a theme component, please use the `upload_theme_component` method instead."
end
theme.set_default! if set_theme_as_default
theme
end
# Uploads a theme component from a directory.
#
# @param parent_theme_id [Integer] The ID of the theme to add the theme component to. Defaults to `SiteSetting.default_theme_id`.
#
# @return [Theme] The uploaded theme model given by `models/theme.rb`.
#
# @example Upload a theme component
# upload_theme_component("/path/to/theme_component")
#
# @example Upload a theme component and add it to a specific theme
# upload_theme_component("/path/to/theme_component", parent_theme_id: 123)
def upload_theme_component(parent_theme_id: SiteSetting.default_theme_id)
theme = RemoteTheme.import_theme_from_directory(theme_dir_from_caller)
if !theme.component
raise "Uploaded theme is not a theme component, please use the `upload_theme` method instead."
end
Theme.find(parent_theme_id).child_themes << theme
theme
end
# Runs named migration for a given theme.
#
# @params [Theme] theme The theme to run the migration for.
# @params [String] migration_name The name of the migration to run.
#
# @return [nil]
#
# @example
# run_theme_migration(theme, "0001-migrate-some-settings")
def run_theme_migration(theme, migration_name)
migration_theme_field = theme.theme_fields.find_by(name: migration_name)
theme.migrate_settings(fields: [migration_theme_field], allow_out_of_sequence_migration: true)
nil
end
private
def theme_dir_from_caller
caller.each do |line|
if (split = line.split(%r{/spec/*/.+_spec.rb})).length > 1
return split.first
end
end
end
end