From b9df0749787016c901e6be83aac47f3528f03ef5 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Mon, 25 Sep 2023 11:24:10 -0400 Subject: [PATCH] Support `file_fixture` in Factory definitions Related to [factory_bot#1282][] [rails/rails#45606][] has been merged and is likely to be released as part of Rails 7.1. With that addition, the path toward resolving [factory_bot#1282][] becomes more clear. If factories can pass along [Pathname][] instances to attachment attributes, Active Support will handle the rest. Instances of `ActiveSupport::TestCase` provide a [file_fixture][] helper to construct a `Pathname` instance based on the path defined by `ActiveSupport::TestCase.file_fixture_path` (relative to the Rails root directory). [factory_bot#1282]: https://github.com/thoughtbot/factory_bot/issues/1282#issuecomment-1733796049 [rails/rails#45606]: https://github.com/rails/rails/pull/45606 [Pathname]: https://docs.ruby-lang.org/en/master/Pathname.html [file_fixture]: https://api.rubyonrails.org/classes/ActiveSupport/Testing/FileFixtures.html#method-i-file_fixture --- README.md | 13 ++++ Rakefile | 5 +- lib/factory_bot_rails/file_fixture_support.rb | 9 +++ lib/factory_bot_rails/railtie.rb | 18 +++++ ...0806125915_create_active_storage_tables.rb | 56 ++++++++++++++++ ...dd_service_name_to_active_storage_blobs.rb | 21 ++++++ ...1_create_active_storage_variant_records.rb | 26 +++++++ ...t_null_on_active_storage_blobs_checksum.rb | 7 ++ spec/factory_bot_rails/factory_spec.rb | 40 +++++++++++ spec/fake_app.rb | 13 ++++ spec/fixtures/files/file.txt | 0 spec/spec_helper.rb | 1 + spec/support/active_record/migrations.rb | 10 +++ spec/support/macros/define_constant.rb | 67 ------------------- test/factory_bot_rails/factory_test.rb | 40 +++++++++++ test/fixtures/files/file.txt | 0 test/test_helper.rb | 11 +++ 17 files changed, 269 insertions(+), 68 deletions(-) create mode 100644 lib/factory_bot_rails/file_fixture_support.rb create mode 100644 spec/dummy/db/migrate/20170806125915_create_active_storage_tables.rb create mode 100644 spec/dummy/db/migrate/20190112182829_add_service_name_to_active_storage_blobs.rb create mode 100644 spec/dummy/db/migrate/20191206030411_create_active_storage_variant_records.rb create mode 100644 spec/dummy/db/migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb create mode 100644 spec/factory_bot_rails/factory_spec.rb create mode 100644 spec/fixtures/files/file.txt create mode 100644 spec/support/active_record/migrations.rb delete mode 100644 spec/support/macros/define_constant.rb create mode 100644 test/factory_bot_rails/factory_test.rb create mode 100644 test/fixtures/files/file.txt create mode 100644 test/test_helper.rb diff --git a/README.md b/README.md index 6aef3130..a1191086 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,19 @@ using an empty array: config.factory_bot.definition_file_paths = [] ``` +### File Fixture Support + +Factories have access to [ActiveSupport::Testing::FileFixtures#file_fixture][] +helper to read files from tests. + +To disable file fixture support, set `file_fixture_support = false`: + +```rb +config.factory_bot.file_fixture_support = false +``` + +[ActiveSupport::Testing::FileFixtures#file_fixture]: https://api.rubyonrails.org/classes/ActiveSupport/Testing/FileFixtures.html#method-i-file_fixture + ### Generators Including factory\_bot\_rails in the development group of your Gemfile diff --git a/Rakefile b/Rakefile index 1b8ba844..ee1006f8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,7 @@ require "bundler/setup" require "cucumber/rake/task" require "rspec/core/rake_task" +require "minitest/test_task" require "standard/rake" Bundler::GemHelper.install_tasks name: "factory_bot_rails" @@ -12,5 +13,7 @@ end RSpec::Core::RakeTask.new(:spec) +Minitest::TestTask.create + desc "Run the test suite and standard" -task default: %w[spec cucumber standard] +task default: %w[spec test cucumber standard] diff --git a/lib/factory_bot_rails/file_fixture_support.rb b/lib/factory_bot_rails/file_fixture_support.rb new file mode 100644 index 00000000..465a91a0 --- /dev/null +++ b/lib/factory_bot_rails/file_fixture_support.rb @@ -0,0 +1,9 @@ +module FactoryBotRails + module FileFixtureSupport + def self.included(klass) + klass.cattr_accessor :file_fixture_support + + klass.delegate :file_fixture, to: "self.class.file_fixture_support" + end + end +end diff --git a/lib/factory_bot_rails/railtie.rb b/lib/factory_bot_rails/railtie.rb index 0e7dfdb8..2b122370 100644 --- a/lib/factory_bot_rails/railtie.rb +++ b/lib/factory_bot_rails/railtie.rb @@ -4,6 +4,7 @@ require "factory_bot_rails/generator" require "factory_bot_rails/reloader" require "factory_bot_rails/factory_validator" +require "factory_bot_rails/file_fixture_support" require "rails" module FactoryBotRails @@ -11,6 +12,7 @@ class Railtie < Rails::Railtie config.factory_bot = ActiveSupport::OrderedOptions.new config.factory_bot.definition_file_paths = FactoryBot.definition_file_paths config.factory_bot.validator = FactoryBotRails::FactoryValidator.new + config.factory_bot.file_fixture_support = true initializer "factory_bot.set_fixture_replacement" do Generator.new(config).run @@ -20,6 +22,22 @@ class Railtie < Rails::Railtie FactoryBot.definition_file_paths = definition_file_paths end + config.after_initialize do + if config.factory_bot.file_fixture_support + FactoryBot::SyntaxRunner.include FactoryBotRails::FileFixtureSupport + + ActiveSupport.on_load :active_support_test_case do + setup { FactoryBot::SyntaxRunner.file_fixture_support = self } + end + + if defined?(RSpec) && RSpec.respond_to?(:configure) + RSpec.configure do |config| + config.before { FactoryBot::SyntaxRunner.file_fixture_support = self } + end + end + end + end + config.after_initialize do |app| FactoryBot.find_definitions Reloader.new(app).run diff --git a/spec/dummy/db/migrate/20170806125915_create_active_storage_tables.rb b/spec/dummy/db/migrate/20170806125915_create_active_storage_tables.rb new file mode 100644 index 00000000..424cc96b --- /dev/null +++ b/spec/dummy/db/migrate/20170806125915_create_active_storage_tables.rb @@ -0,0 +1,56 @@ +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [ primary_key_type, foreign_key_type ] + end +end diff --git a/spec/dummy/db/migrate/20190112182829_add_service_name_to_active_storage_blobs.rb b/spec/dummy/db/migrate/20190112182829_add_service_name_to_active_storage_blobs.rb new file mode 100644 index 00000000..6076d58b --- /dev/null +++ b/spec/dummy/db/migrate/20190112182829_add_service_name_to_active_storage_blobs.rb @@ -0,0 +1,21 @@ +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + return unless table_exists?(:active_storage_blobs) + + unless column_exists?(:active_storage_blobs, :service_name) + add_column :active_storage_blobs, :service_name, :string + + if configured_service = ActiveStorage::Blob.service.name + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + end + + def down + return unless table_exists?(:active_storage_blobs) + + remove_column :active_storage_blobs, :service_name + end +end diff --git a/spec/dummy/db/migrate/20191206030411_create_active_storage_variant_records.rb b/spec/dummy/db/migrate/20191206030411_create_active_storage_variant_records.rb new file mode 100644 index 00000000..0ff7345c --- /dev/null +++ b/spec/dummy/db/migrate/20191206030411_create_active_storage_variant_records.rb @@ -0,0 +1,26 @@ +class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + # Use Active Record's configured type for primary key + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| + t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type + t.string :variation_digest, null: false + + t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end +end diff --git a/spec/dummy/db/migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb b/spec/dummy/db/migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb new file mode 100644 index 00000000..d00315e7 --- /dev/null +++ b/spec/dummy/db/migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb @@ -0,0 +1,7 @@ +class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + change_column_null(:active_storage_blobs, :checksum, true) + end +end diff --git a/spec/factory_bot_rails/factory_spec.rb b/spec/factory_bot_rails/factory_spec.rb new file mode 100644 index 00000000..479ff1f9 --- /dev/null +++ b/spec/factory_bot_rails/factory_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "factory extensions" do + describe "#file_fixture" do + it "delegates to the test harness" do + FactoryBot.define do + factory :upload, class: Struct.new(:filename) do + filename { file_fixture("file.txt") } + end + end + + upload = FactoryBot.build(:upload) + + expect(Pathname(upload.filename)).to eq(file_fixture("file.txt")) + end + + it "uploads an ActiveStorage::Blob" do + FactoryBot.define do + factory :active_storage_blob, class: ActiveStorage::Blob do + filename { pathname.basename } + + transient do + pathname { file_fixture("file.txt") } + end + + after :build do |model, factory| + model.upload factory.pathname.open + end + end + end + + blob = FactoryBot.create(:active_storage_blob) + + expect(blob.filename.to_s).to eq("file.txt") + expect(blob.download).to eq(file_fixture("file.txt").read) + end + end +end diff --git a/spec/fake_app.rb b/spec/fake_app.rb index 958850a1..7f329893 100644 --- a/spec/fake_app.rb +++ b/spec/fake_app.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +ENV["DATABASE_URL"] = "sqlite3::memory:" + +require "active_record/railtie" +require "active_storage/engine" + module Dummy class Application < Rails::Application config.eager_load = false @@ -8,6 +13,14 @@ class Application < Rails::Application if Rails.gem_version >= Gem::Version.new("7.1") config.active_support.cache_format_version = 7 end + + config.active_storage.service = :local + config.active_storage.service_configurations = { + local: { + root: root.join("tmp/storage"), + service: "Disk" + } + } end end diff --git a/spec/fixtures/files/file.txt b/spec/fixtures/files/file.txt new file mode 100644 index 00000000..e69de29b diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1c77212d..725f2be4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require "factory_bot_rails" require "fake_app" +require "rspec/rails" Dir["spec/support/**/*.rb"].each { |f| require File.expand_path(f) } diff --git a/spec/support/active_record/migrations.rb b/spec/support/active_record/migrations.rb new file mode 100644 index 00000000..ef98be7a --- /dev/null +++ b/spec/support/active_record/migrations.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Rails.root.glob("../dummy/db/migrate/*.rb").each do |file| + require file + + if file.read =~ /\Aclass (\w+)/ && (migration = Regexp.last_match(1).safe_constantize) + migration.verbose = false + migration.migrate :up + end +end diff --git a/spec/support/macros/define_constant.rb b/spec/support/macros/define_constant.rb deleted file mode 100644 index 5184f29f..00000000 --- a/spec/support/macros/define_constant.rb +++ /dev/null @@ -1,67 +0,0 @@ -require "active_record" - -module DefineConstantMacros - def define_class(path, base = Object, &block) - const = stub_const(path, Class.new(base)) - const.class_eval(&block) if block - const - end - - def define_model(name, columns = {}, &) - model = define_class(name, ActiveRecord::Base, &) - create_table(model.table_name) do |table| - columns.each do |column_name, type| - table.column column_name, type - end - end - model - end - - def create_table(table_name, &) - connection = ActiveRecord::Base.connection - - begin - connection.execute("DROP TABLE IF EXISTS #{table_name}") - connection.create_table(table_name, &) - created_tables << table_name - connection - rescue Exception => e # rubocop:disable Lint/RescueException - connection.execute("DROP TABLE IF EXISTS #{table_name}") - raise e - end - end - - def clear_generated_tables - created_tables.each do |table_name| - clear_generated_table(table_name) - end - created_tables.clear - end - - def clear_generated_table(table_name) - ActiveRecord::Base - .connection - .execute("DROP TABLE IF EXISTS #{table_name}") - end - - private - - def created_tables - @created_tables ||= [] - end -end - -RSpec.configure do |config| - config.include DefineConstantMacros - - config.before(:all) do - ActiveRecord::Base.establish_connection( - adapter: "sqlite3", - database: ":memory:" - ) - end - - config.after do - clear_generated_tables - end -end diff --git a/test/factory_bot_rails/factory_test.rb b/test/factory_bot_rails/factory_test.rb new file mode 100644 index 00000000..0c657ef7 --- /dev/null +++ b/test/factory_bot_rails/factory_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "test_helper" + +class FactoryBotRails::FactoryTest < ActiveSupport::TestCase + self.file_fixture_path = "test/fixtures/files" + + test "delegates #file_fixture to the test harness" do + FactoryBot.define do + factory :upload, class: Struct.new(:filename) do + filename { file_fixture("file.txt") } + end + end + + upload = FactoryBot.build(:upload) + + assert_equal file_fixture("file.txt"), upload.filename + end + + test "uploads an ActiveStorage::Blob" do + FactoryBot.define do + factory :active_storage_blob, class: ActiveStorage::Blob do + filename { pathname.basename } + + transient do + pathname { file_fixture("file.txt") } + end + + after :build do |model, factory| + model.upload factory.pathname.open + end + end + end + + blob = FactoryBot.create(:active_storage_blob) + + assert_equal "file.txt", blob.filename.to_s + assert_equal file_fixture("file.txt").read, blob.download + end +end diff --git a/test/fixtures/files/file.txt b/test/fixtures/files/file.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..b9f45a5b --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require_relative "../spec/fake_app" + +require "rails/test_help" +require "factory_bot_rails" + +Dir["spec/support/**/*.rb"].each { |f| require File.expand_path(f) }