split up migration to domain spec

This commit is contained in:
Dylan Knutson
2025-02-05 17:49:45 +00:00
parent 35ba1db97e
commit 4bb0eae722
19 changed files with 1315 additions and 1047 deletions

View File

@@ -158,7 +158,11 @@ class Domain::MigrateToDomain
logger.info "migrating fa followed users"
Domain::User::FaUser
.where(migrated_followed_users_at: nil)
.find_each { |user| migrate_fa_user_followed_users(user) }
.find_each do |user|
ReduxApplicationRecord.transaction do
migrate_fa_user_followed_users(user)
end
end
end
sig { void }
@@ -520,7 +524,7 @@ class Domain::MigrateToDomain
)
end
if user.following_users.count != old_user.follows.count
if new_user_ids.size != old_user.follows.count
logger.error(
"followers mismatch for #{user.name}: (#{user.following_users.count} != #{old_user.follows.count})",
)

View File

@@ -74,12 +74,22 @@ class Domain::Post < ReduxApplicationRecord
source: :user
end
sig { params(group_assoc_name: Symbol).void }
def self.belongs_to_groups!(group_assoc_name: :groups)
sig do
params(
group_assoc_name: Symbol,
group_klass: T.class_of(Domain::PostGroup),
join_klass: T.class_of(Domain::PostGroupJoin),
).void
end
def self.belongs_to_groups!(group_assoc_name, group_klass, join_klass)
has_many :post_group_joins,
class_name: "::Domain::PostGroupJoin",
class_name: join_klass.name,
inverse_of: :post,
dependent: :destroy
has_many group_assoc_name, through: :post_group_joins, source: :group
has_many group_assoc_name,
through: :post_group_joins,
source: :group,
class_name: group_klass.name
end
end

View File

@@ -33,7 +33,9 @@ class Domain::Post::E621Post < Domain::Post
has_single_file!
has_faving_users! Domain::User::E621User
belongs_to_groups! group_assoc_name: :pools
belongs_to_groups! :pools,
Domain::PostGroup::E621Pool,
Domain::PostGroupJoin::E621PoolJoin
belongs_to :parent_post,
class_name: "Domain::Post::E621Post",

View File

@@ -7,7 +7,9 @@ class Domain::Post::InkbunnyPost < Domain::Post
has_single_creator! Domain::User::InkbunnyUser
has_faving_users! Domain::User::InkbunnyUser
belongs_to_groups! group_assoc_name: :pools
belongs_to_groups! :pools,
Domain::PostGroup::InkbunnyPool,
Domain::PostGroupJoin::InkbunnyPoolJoin
validates :ib_id, presence: true
validates :state, presence: true, inclusion: { in: %w[ok error] }

View File

@@ -45,8 +45,40 @@ class Domain::User < ReduxApplicationRecord
has_many :posts, through: :user_post_creations, source: :post
has_many :faved_posts, through: :user_post_favs, source: :post
has_many :following_users, through: :user_user_follows_to, source: :to
has_many :followed_by_users, through: :user_user_follows_from, source: :from
has_many :following_users, through: :user_user_follows_from, source: :to
has_many :followed_by_users, through: :user_user_follows_to, source: :from
sig { params(klass: T.class_of(Domain::Post)).void }
def self.has_created_posts!(klass)
has_many :posts,
through: :user_post_creations,
source: :post,
class_name: klass.name
end
sig { params(klass: T.class_of(Domain::Post)).void }
def self.has_faved_posts!(klass)
has_many :faved_posts,
through: :user_post_favs,
source: :post,
class_name: klass.name
end
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_followed_users!(klass)
has_many :following_users,
through: :user_user_follows_from,
source: :to,
class_name: klass.name
end
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_followed_by_users!(klass)
has_many :followed_by_users,
through: :user_user_follows_to,
source: :from,
class_name: klass.name
end
has_one :avatar,
-> { order(created_at: :desc) },

View File

@@ -20,6 +20,8 @@ class Domain::User::E621User < Domain::User
validates :e621_id, presence: true
validates :name, length: { minimum: 1 }, allow_nil: false
has_faved_posts! Domain::Post::E621Post
sig { override.returns(T.nilable(String)) }
def to_param
"e621/#{e621_id}" if e621_id.present?

View File

@@ -20,6 +20,11 @@ class Domain::User::FaUser < Domain::User
validates :name, presence: true
validates :url_name, presence: true
has_followed_users! Domain::User::FaUser
has_followed_by_users! Domain::User::FaUser
has_created_posts! Domain::Post::FaPost
has_faved_posts! Domain::Post::FaPost
sig { override.returns(T.nilable(String)) }
def to_param
"fa/#{url_name}" if url_name.present?

View File

@@ -548,10 +548,10 @@ class Domain::Post::E621Post
# This method is created by ActiveRecord on the `Domain::Post::E621Post` class because it declared `has_many :pools, through: :post_group_joins`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::PostGroup::PrivateCollectionProxy) }
sig { returns(::Domain::PostGroup::E621Pool::PrivateCollectionProxy) }
def pools; end
sig { params(value: T::Enumerable[::Domain::PostGroup]).void }
sig { params(value: T::Enumerable[::Domain::PostGroup::E621Pool]).void }
def pools=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -562,10 +562,10 @@ class Domain::Post::E621Post
# This method is created by ActiveRecord on the `Domain::Post::E621Post` class because it declared `has_many :post_group_joins`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostGroupJoin::PrivateCollectionProxy) }
sig { returns(::Domain::PostGroupJoin::E621PoolJoin::PrivateCollectionProxy) }
def post_group_joins; end
sig { params(value: T::Enumerable[::Domain::PostGroupJoin]).void }
sig { params(value: T::Enumerable[::Domain::PostGroupJoin::E621PoolJoin]).void }
def post_group_joins=(value); end
sig { returns(T.nilable(::Domain::PostFile)) }

View File

@@ -508,10 +508,10 @@ class Domain::Post::InkbunnyPost
# This method is created by ActiveRecord on the `Domain::Post::InkbunnyPost` class because it declared `has_many :pools, through: :post_group_joins`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::PostGroup::PrivateCollectionProxy) }
sig { returns(::Domain::PostGroup::InkbunnyPool::PrivateCollectionProxy) }
def pools; end
sig { params(value: T::Enumerable[::Domain::PostGroup]).void }
sig { params(value: T::Enumerable[::Domain::PostGroup::InkbunnyPool]).void }
def pools=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -522,10 +522,10 @@ class Domain::Post::InkbunnyPost
# This method is created by ActiveRecord on the `Domain::Post::InkbunnyPost` class because it declared `has_many :post_group_joins`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostGroupJoin::PrivateCollectionProxy) }
sig { returns(::Domain::PostGroupJoin::InkbunnyPoolJoin::PrivateCollectionProxy) }
def post_group_joins; end
sig { params(value: T::Enumerable[::Domain::PostGroupJoin]).void }
sig { params(value: T::Enumerable[::Domain::PostGroupJoin::InkbunnyPoolJoin]).void }
def post_group_joins=(value); end
sig { returns(T.nilable(::Domain::UserPostCreation)) }

View File

@@ -460,7 +460,7 @@ class Domain::User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def followed_by_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_from`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_to`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def followed_by_users; end
@@ -474,7 +474,7 @@ class Domain::User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def following_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_to`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_from`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def following_users; end

View File

@@ -478,12 +478,12 @@ class Domain::User::E621User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def faved_post_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# This method is created by ActiveRecord on the `Domain::User::E621User` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::Post::PrivateCollectionProxy) }
sig { returns(::Domain::Post::E621Post::PrivateCollectionProxy) }
def faved_posts; end
sig { params(value: T::Enumerable[::Domain::Post]).void }
sig { params(value: T::Enumerable[::Domain::Post::E621Post]).void }
def faved_posts=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -492,7 +492,7 @@ class Domain::User::E621User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def followed_by_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_from`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_to`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def followed_by_users; end
@@ -506,7 +506,7 @@ class Domain::User::E621User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def following_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_to`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_from`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def following_users; end

View File

@@ -478,12 +478,12 @@ class Domain::User::FaUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def faved_post_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# This method is created by ActiveRecord on the `Domain::User::FaUser` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::Post::PrivateCollectionProxy) }
sig { returns(::Domain::Post::FaPost::PrivateCollectionProxy) }
def faved_posts; end
sig { params(value: T::Enumerable[::Domain::Post]).void }
sig { params(value: T::Enumerable[::Domain::Post::FaPost]).void }
def faved_posts=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -492,12 +492,12 @@ class Domain::User::FaUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def followed_by_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_from`.
# This method is created by ActiveRecord on the `Domain::User::FaUser` class because it declared `has_many :followed_by_users, through: :user_user_follows_to`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
sig { returns(::Domain::User::FaUser::PrivateCollectionProxy) }
def followed_by_users; end
sig { params(value: T::Enumerable[::Domain::User]).void }
sig { params(value: T::Enumerable[::Domain::User::FaUser]).void }
def followed_by_users=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -506,12 +506,12 @@ class Domain::User::FaUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def following_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_to`.
# This method is created by ActiveRecord on the `Domain::User::FaUser` class because it declared `has_many :following_users, through: :user_user_follows_from`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
sig { returns(::Domain::User::FaUser::PrivateCollectionProxy) }
def following_users; end
sig { params(value: T::Enumerable[::Domain::User]).void }
sig { params(value: T::Enumerable[::Domain::User::FaUser]).void }
def following_users=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -520,12 +520,12 @@ class Domain::User::FaUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def post_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :posts, through: :user_post_creations`.
# This method is created by ActiveRecord on the `Domain::User::FaUser` class because it declared `has_many :posts, through: :user_post_creations`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::Post::PrivateCollectionProxy) }
sig { returns(::Domain::Post::FaPost::PrivateCollectionProxy) }
def posts; end
sig { params(value: T::Enumerable[::Domain::Post]).void }
sig { params(value: T::Enumerable[::Domain::Post::FaPost]).void }
def posts=(value); end
sig { returns(T.nilable(::Domain::UserAvatar)) }

View File

@@ -527,7 +527,7 @@ class Domain::User::InkbunnyUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def followed_by_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_from`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :followed_by_users, through: :user_user_follows_to`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def followed_by_users; end
@@ -541,7 +541,7 @@ class Domain::User::InkbunnyUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def following_user_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_to`.
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :following_users, through: :user_user_follows_from`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::User::PrivateCollectionProxy) }
def following_users; end

View File

@@ -2,9 +2,11 @@
FactoryBot.define do
factory :domain_fa_user_avatar, class: "Domain::Fa::UserAvatar" do
association :user, factory: :domain_fa_user
association :file, factory: :http_log_entry
state { :ok }
state_detail { {} }
file_url_str { "https://example.com/avatar.jpg" }
association :log_entry, factory: :http_log_entry
after(:build) { |avatar| avatar.file = avatar.log_entry&.response }
end
end

View File

@@ -0,0 +1,210 @@
# typed: false
require "rails_helper"
require_relative "./shared_examples"
RSpec.describe Domain::MigrateToDomain do
include_context "migrate_to_domain"
def expect_users_match(old_user, new_user)
expect(new_user).to have_attributes(
e621_id: old_user.e621_user_id,
name: old_user.name,
favs_are_hidden: old_user.favs_are_hidden,
num_other_favs_cached: old_user.num_other_favs_cached,
scanned_favs_status: old_user.scanned_favs_status,
scanned_favs_at: be_within(1.second).of(old_user.scanned_favs_at),
)
end
def expect_posts_match(old_post, new_post)
expect(new_post).to have_attributes(
state: old_post.state,
e621_id: old_post.e621_id,
scanned_post_favs_at: old_post.scanned_post_favs_at,
rating: old_post.rating,
tags_array: old_post.tags_array,
flags_array: old_post.flags_array,
pools_array: old_post.pools_array,
sources_array: old_post.sources_array,
artists_array: old_post.artists_array,
e621_updated_at: be_within(1.second).of(old_post.e621_updated_at),
last_index_page_id: old_post.last_index_page_id,
caused_by_entry_id: old_post.caused_by_entry_id,
scan_log_entry_id: old_post.scan_log_entry_id,
index_page_ids: old_post.index_page_ids,
prev_md5s: old_post.prev_md5s,
scan_error: old_post.scan_error,
file_error: old_post.file_error,
created_at: be_within(1.second).of(old_post.created_at),
parent_post_e621_id: old_post.parent_e621_id,
)
end
describe "#migrate_e621_users" do
let!(:old_user) do
Domain::E621::User.create!(
e621_user_id: 123,
name: "test_user",
favs_are_hidden: true,
num_other_favs_cached: 42,
scanned_favs_status: "ok",
scanned_favs_at: Time.current,
)
end
it "migrates users that don't exist in the new table" do
expect { migrator.migrate_e621_users }.to change(
Domain::User::E621User,
:count,
).by(1)
new_user = Domain::User::E621User.find_by(e621_id: old_user.e621_user_id)
expect_users_match(old_user, new_user)
end
it "skips users that already exist in the new table" do
# Create a user in the new table first
Domain::User::E621User.create!(
e621_id: old_user.e621_user_id,
name: old_user.name,
)
expect { migrator.migrate_e621_users }.not_to change(
Domain::User::E621User,
:count,
)
end
it "handles multiple users in batches" do
# Create a few more old users
additional_users =
2.times.map do |i|
Domain::E621::User.create!(
e621_user_id: 456 + i,
name: "test_user_#{i}",
favs_are_hidden: false,
num_other_favs_cached: i,
scanned_favs_status: "ok",
scanned_favs_at: Time.current,
)
end
expect { migrator.migrate_e621_users }.to change(
Domain::User::E621User,
:count,
).by(3)
expect(Domain::User::E621User.count).to eq(3)
expect(Domain::User::E621User.pluck(:e621_id)).to contain_exactly(
123,
456,
457,
)
# Verify all users were migrated correctly
([old_user] + additional_users).each do |old_user|
new_user =
Domain::User::E621User.find_by(e621_id: old_user.e621_user_id)
expect_users_match(old_user, new_user)
end
end
end
describe "#migrate_e621_posts" do
let!(:old_post) do
Domain::E621::Post.create!(
e621_id: 123,
state: "ok",
rating: "s",
tags_array: {
"general" => %w[tag1 tag2],
},
flags_array: ["flag1"],
pools_array: ["pool1"],
sources_array: ["source1"],
artists_array: ["artist1"],
e621_updated_at: Time.current,
last_index_page_id: 1,
caused_by_entry_id: 2,
scan_log_entry_id: 3,
index_page_ids: [1, 2, 3],
prev_md5s: ["md5_1"],
scan_error: nil,
file_error: nil,
parent_e621_id: nil,
scanned_post_favs_at: Time.current,
)
end
it "migrates posts that don't exist in the new table" do
expect { migrator.migrate_e621_posts }.to change(
Domain::Post::E621Post,
:count,
).by(1)
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect_posts_match(old_post, new_post)
end
it "skips posts that already exist in the new table" do
# Create a post in the new table first
Domain::Post::E621Post.create!(
e621_id: old_post.e621_id,
state: "ok",
rating: "q",
)
expect { migrator.migrate_e621_posts }.not_to change(
Domain::Post::E621Post,
:count,
)
end
it "handles multiple posts in batches" do
# Create a few more old posts
additional_posts =
2.times.map do |i|
Domain::E621::Post.create!(
e621_id: 456 + i,
state: "ok",
rating: "q",
tags_array: {
"general" => ["tag#{i}"],
},
flags_array: ["flag#{i}"],
pools_array: ["pool#{i}"],
sources_array: ["source#{i}"],
artists_array: ["artist#{i}"],
e621_updated_at: Time.current,
last_index_page_id: i,
caused_by_entry_id: i + 1,
scan_log_entry_id: i + 2,
index_page_ids: [i],
prev_md5s: ["md5_#{i}"],
scan_error: nil,
file_error: nil,
parent_e621_id: nil,
scanned_post_favs_at: Time.current,
)
end
expect { migrator.migrate_e621_posts }.to change(
Domain::Post::E621Post,
:count,
).by(3)
expect(Domain::Post::E621Post.count).to eq(3)
expect(Domain::Post::E621Post.pluck(:e621_id)).to contain_exactly(
123,
456,
457,
)
# Verify all posts were migrated correctly
([old_post] + additional_posts).each do |old_post|
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect_posts_match(old_post, new_post)
end
end
end
end

View File

@@ -0,0 +1,495 @@
# typed: false
require "rails_helper"
require_relative "./shared_examples"
RSpec.describe Domain::MigrateToDomain do
include_context "migrate_to_domain"
def expect_fa_users_match(old_user, new_user)
expect(new_user).to have_attributes(
url_name: old_user.url_name,
name: old_user.name,
full_name: old_user.full_name,
artist_type: old_user.artist_type,
mood: old_user.mood,
profile_html: old_user.profile_html,
num_pageviews: old_user.num_pageviews,
num_submissions: old_user.num_submissions,
num_comments_recieved: old_user.num_comments_recieved,
num_comments_given: old_user.num_comments_given,
num_journals: old_user.num_journals,
num_favorites: old_user.num_favorites,
scanned_gallery_at: be_within(1.second).of(old_user.scanned_gallery_at),
scanned_page_at: be_within(1.second).of(old_user.scanned_page_at),
registered_at: be_within(1.second).of(old_user.registered_at),
)
if old_user.avatar.present?
expect(new_user.avatar).to have_attributes(
log_entry_id: old_user.avatar.log_entry_id,
url_str: old_user.avatar.file_url_str,
state: old_user.avatar.state == "ok" ? "ok" : "error",
)
if old_user.avatar.state != "ok"
expect(new_user.avatar.error_message).to eq(old_user.avatar.state)
end
else
expect(new_user.avatar).to be_nil
end
end
def expect_fa_posts_match(old_post, new_post)
expect(new_post).to have_attributes(
state: old_post.state,
title: old_post.title,
fa_id: old_post.fa_id,
category: old_post.category,
theme: old_post.theme,
species: old_post.species,
gender: old_post.gender,
description: old_post.description,
keywords: old_post.keywords,
num_favorites: old_post.num_favorites,
num_comments: old_post.num_comments,
num_views: old_post.num_views,
posted_at: be_within(1.second).of(old_post.posted_at),
scanned_at: be_within(1.second).of(old_post.scanned_at),
scan_file_error: old_post.scan_file_error,
last_user_page_id: old_post.last_user_page_id,
last_submission_page_id: old_post.last_submission_page_id,
first_browse_page_id: old_post.first_browse_page_id,
first_gallery_page_id: old_post.first_gallery_page_id,
first_seen_entry_id: old_post.first_seen_entry_id,
created_at: be_within(1.second).of(old_post.created_at),
)
if old_post.creator.present?
expect(new_post.creator).to have_attributes(
url_name: old_post.creator.url_name,
)
else
expect(new_post.creator).to be_nil
end
if old_post.file.present?
expect(new_post.file).to have_attributes(
log_entry_id: old_post.file_id,
url_str: old_post.file_url_str,
state: old_post.state,
)
else
expect(new_post.file).to be_nil
end
end
describe "#migrate_fa_users" do
let!(:old_user) do
Domain::Fa::User.create!(
url_name: "testuser",
name: "Test_User",
full_name: "Test User Full Name",
artist_type: "artist",
mood: "happy",
profile_html: "<p>Test profile</p>",
num_pageviews: 1000,
num_submissions: 50,
num_comments_recieved: 200,
num_comments_given: 150,
num_journals: 10,
num_favorites: 300,
scanned_gallery_at: Time.current,
scanned_page_at: Time.current,
registered_at: 1.year.ago,
)
end
it "migrates users that don't exist in the new table" do
expect { migrator.migrate_fa_users }.to change(
Domain::User::FaUser,
:count,
).by(1)
new_user = Domain::User::FaUser.find_by(url_name: old_user.url_name)
expect_fa_users_match(old_user, new_user)
end
it "skips users that already exist in the new table" do
# Create a user in the new table first
Domain::User::FaUser.create!(
url_name: old_user.url_name,
name: old_user.name,
)
expect { migrator.migrate_fa_users }.not_to change(
Domain::User::FaUser,
:count,
)
end
it "handles multiple users in batches" do
# Create a few more old users
additional_users =
2.times.map do |i|
Domain::Fa::User.create!(
url_name: "testuser#{i}",
name: "Test_User_#{i}",
full_name: "Test User #{i} Full Name",
artist_type: "artist",
mood: "happy",
profile_html: "<p>Test profile #{i}</p>",
num_pageviews: 1000 + i,
num_submissions: 50 + i,
num_comments_recieved: 200 + i,
num_comments_given: 150 + i,
num_journals: 10 + i,
num_favorites: 300 + i,
scanned_gallery_at: Time.current,
scanned_page_at: Time.current,
registered_at: i.days.ago,
)
end
expect { migrator.migrate_fa_users }.to change(
Domain::User::FaUser,
:count,
).by(3)
expect(Domain::User::FaUser.count).to eq(3)
expect(Domain::User::FaUser.pluck(:url_name)).to contain_exactly(
"testuser",
"testuser0",
"testuser1",
)
# Verify all users were migrated correctly
([old_user] + additional_users).each do |old_user|
new_user = Domain::User::FaUser.find_by(url_name: old_user.url_name)
expect_fa_users_match(old_user, new_user)
end
end
it "handles users with avatars" do
avatar = create(:domain_fa_user_avatar, user: old_user)
old_user.avatar = avatar
old_user.save!
migrator.migrate_fa_users
new_user = Domain::User::FaUser.find_by(url_name: old_user.url_name)
expect_fa_users_match(old_user, new_user)
expect(new_user.avatar).to be_present
expect(new_user.avatar.url_str).to eq(avatar.file_url_str)
expect(new_user.avatar.state).to eq("ok")
end
it "handles users with errored avatars" do
avatar =
create(:domain_fa_user_avatar, state: "download_error", user: old_user)
old_user.avatar = avatar
old_user.save!
migrator.migrate_fa_users
new_user = Domain::User::FaUser.find_by(url_name: old_user.url_name)
expect_fa_users_match(old_user, new_user)
expect(new_user.avatar).to be_present
expect(new_user.avatar.url_str).to eq(avatar.file_url_str)
expect(new_user.avatar.state).to eq("error")
expect(new_user.avatar.error_message).to eq("download_error")
end
end
describe "#migrate_fa_posts" do
let!(:creator) do
Domain::Fa::User.create!(url_name: "artist1", name: "Artist 1")
end
let!(:new_creator) do
Domain::User::FaUser.create!(url_name: "artist1", name: "Artist 1")
end
let!(:old_post) do
Domain::Fa::Post.create!(
fa_id: 123,
state: "ok",
title: "Test Post",
category: "artwork",
theme: "abstract",
species: "canine",
gender: "male",
description: "Test description",
keywords: %w[test art],
num_favorites: 42,
num_comments: 10,
num_views: 100,
posted_at: Time.current,
scanned_at: Time.current,
scan_file_error: nil,
last_user_page_id: 1,
last_submission_page_id: 2,
first_browse_page_id: 3,
first_gallery_page_id: 4,
first_seen_entry_id: 5,
creator: creator,
file_url_str: "https://example.com/image.jpg",
file: create(:http_log_entry),
)
end
it "migrates posts that don't exist in the new table" do
expect { migrator.migrate_fa_posts }.to change(
Domain::Post::FaPost,
:count,
).by(1)
new_post = Domain::Post::FaPost.find_by(fa_id: old_post.fa_id)
expect_fa_posts_match(old_post, new_post)
end
it "skips posts that already exist in the new table" do
# Create a post in the new table first
Domain::Post::FaPost.create!(fa_id: old_post.fa_id, state: "ok")
expect { migrator.migrate_fa_posts }.not_to change(
Domain::Post::FaPost,
:count,
)
end
it "handles multiple posts in batches" do
# Create a few more old posts
additional_posts =
2.times.map do |i|
Domain::Fa::Post.create!(
fa_id: 456 + i,
state: "ok",
title: "Test Post #{i}",
category: "artwork",
theme: "abstract",
species: "canine",
gender: "male",
description: "Test description #{i}",
keywords: ["test#{i}", "art"],
num_favorites: 42 + i,
num_comments: 10 + i,
num_views: 100 + i,
posted_at: Time.current,
scanned_at: Time.current,
scan_file_error: nil,
last_user_page_id: i + 1,
last_submission_page_id: i + 2,
first_browse_page_id: i + 3,
first_gallery_page_id: i + 4,
first_seen_entry_id: i + 5,
creator: creator,
file_url_str: "https://example.com/image_#{i}.jpg",
file: create(:http_log_entry),
)
end
expect { migrator.migrate_fa_posts }.to change(
Domain::Post::FaPost,
:count,
).by(3)
expect(Domain::Post::FaPost.count).to eq(3)
expect(Domain::Post::FaPost.pluck(:fa_id)).to contain_exactly(
123,
456,
457,
)
# Verify all posts were migrated correctly
([old_post] + additional_posts).each do |old_post|
new_post = Domain::Post::FaPost.find_by(fa_id: old_post.fa_id)
expect_fa_posts_match(old_post, new_post)
end
end
it "handles posts without creators" do
post_without_creator =
Domain::Fa::Post.create!(
fa_id: 789,
state: "ok",
title: "No Creator Post",
category: "artwork",
posted_at: Time.current,
scanned_at: Time.current,
)
expect { migrator.migrate_fa_posts }.to change(
Domain::Post::FaPost,
:count,
).by(2)
new_post = Domain::Post::FaPost.find_by(fa_id: post_without_creator.fa_id)
expect_fa_posts_match(post_without_creator, new_post)
end
it "handles posts without files" do
post_without_file =
Domain::Fa::Post.create!(
fa_id: 789,
state: "ok",
title: "No File Post",
category: "artwork",
posted_at: Time.current,
scanned_at: Time.current,
creator: creator,
)
expect { migrator.migrate_fa_posts }.to change(
Domain::Post::FaPost,
:count,
).by(2)
new_post = Domain::Post::FaPost.find_by(fa_id: post_without_file.fa_id)
expect_fa_posts_match(post_without_file, new_post)
end
end
describe "#migrate_fa_users_favs" do
let!(:old_user) do
Domain::Fa::User.create!(url_name: "testuser", name: "Test_User")
end
let!(:new_user) do
Domain::User::FaUser.create!(url_name: "testuser", name: "Test_User")
end
let!(:old_posts) do
3.times.map do |i|
Domain::Fa::Post.create!(
fa_id: i + 1,
state: "ok",
title: "Test Post #{i}",
category: "artwork",
posted_at: Time.current,
scanned_at: Time.current,
)
end
end
let!(:new_posts) do
old_posts.map do |old_post|
Domain::Post::FaPost.create!(
fa_id: old_post.fa_id,
state: "ok",
title: old_post.title,
)
end
end
before do
# Add favs to the old user
old_user.fav_posts = old_posts
old_user.save!
end
it "migrates user favs correctly" do
expect { migrator.migrate_fa_users_favs }.to change(
Domain::UserPostFav,
:count,
).by(3)
new_user.reload
expect(new_user.faved_posts.count).to eq(3)
expect(new_user.faved_posts.pluck(:fa_id)).to contain_exactly(1, 2, 3)
expect(new_user.migrated_user_favs_at).to be_present
end
it "skips users that have already been migrated" do
new_user.update!(migrated_user_favs_at: Time.current)
expect { migrator.migrate_fa_users_favs }.not_to change(
Domain::UserPostFav,
:count,
)
end
it "handles missing posts gracefully" do
# Delete one of the new posts to simulate a missing post
new_posts.first.destroy
expect { migrator.migrate_fa_users_favs }.to change(
Domain::UserPostFav,
:count,
).by(2)
expect(new_user.faved_posts.count).to eq(2)
expect(new_user.migrated_user_favs_at).to be_nil
end
end
describe "#migrate_fa_users_followed_users" do
let!(:old_user) do
Domain::Fa::User.create!(url_name: "testuser", name: "Test_User")
end
let!(:new_user) do
Domain::User::FaUser.create!(url_name: "testuser", name: "Test_User")
end
let!(:old_followed_users) do
3.times.map do |i|
Domain::Fa::User.create!(
url_name: "followeduser#{i}",
name: "Followed User #{i}",
)
end
end
let!(:new_followed_users) do
old_followed_users.map do |old_followed_user|
Domain::User::FaUser.create!(
url_name: old_followed_user.url_name,
name: old_followed_user.name,
)
end
end
before do
# Add follows to the old user
old_user.follows = old_followed_users
old_user.save!
end
it "migrates user follows correctly" do
expect { migrator.migrate_fa_users_followed_users }.to change(
Domain::UserUserFollow,
:count,
).by(3)
expect(new_user.following_users.count).to eq(3)
new_user.reload
expect(new_user.following_users.pluck(:url_name)).to contain_exactly(
"followeduser0",
"followeduser1",
"followeduser2",
)
expect(new_user.migrated_followed_users_at).to be_present
end
it "skips users that have already been migrated" do
new_user.update!(migrated_followed_users_at: Time.current)
expect { migrator.migrate_fa_users_followed_users }.not_to change(
Domain::UserUserFollow,
:count,
)
end
it "handles missing followed users gracefully" do
# Delete one of the new followed users to simulate a missing user
new_followed_users.first.destroy
expect { migrator.migrate_fa_users_followed_users }.to change(
Domain::UserUserFollow,
:count,
).by(2)
expect(new_user.following_users.count).to eq(2)
expect(new_user.migrated_followed_users_at).to be_nil
end
end
end

View File

@@ -0,0 +1,504 @@
# typed: false
require "rails_helper"
require_relative "./shared_examples"
RSpec.describe Domain::MigrateToDomain do
include_context "migrate_to_domain"
def expect_inkbunny_users_match(old_user, new_user)
expect(new_user).to have_attributes(
ib_id: old_user.ib_user_id,
name: old_user.name,
state: old_user.state.to_s,
scanned_gallery_at:
be_within(1.second).of(old_user.scanned_gallery_at) || be_nil,
deep_update_log_entry_id: old_user.deep_update_log_entry_id,
shallow_update_log_entry_id: old_user.shallow_update_log_entry_id,
created_at: be_within(1.second).of(old_user.created_at),
)
if old_user.avatar_log_entry.present?
expect(new_user.avatar).to have_attributes(
log_entry_id: old_user.avatar_log_entry.id,
url_str: old_user.avatar_log_entry.uri_str,
state: old_user.avatar_log_entry.status_code == 200 ? "ok" : "error",
)
else
expect(new_user.avatar).to be_nil
end
end
def expect_inkbunny_posts_match(old_post, new_post)
expect(new_post).to have_attributes(
state: old_post.state,
ib_id: old_post.ib_post_id,
rating: old_post.rating,
submission_type: old_post.submission_type,
created_at: be_within(1.second).of(old_post.created_at),
)
if old_post.creator.present?
expect(new_post.creator).to have_attributes(
ib_id: old_post.creator.ib_user_id,
)
else
expect(new_post.creator).to be_nil
end
if old_post.files.present?
expect(new_post.files.map(&:log_entry_id)).to match_array(
old_post.files.map(&:log_entry_id),
)
expect(new_post.files.map(&:url_str)).to match_array(
old_post.files.map(&:url_str),
)
expect(new_post.files.map(&:state)).to match_array(
old_post.files.map(&:state),
)
else
expect(new_post.files).to be_empty
end
end
def expect_inkbunny_pools_match(old_pool, new_pool)
expect(new_pool).to have_attributes(ib_id: old_pool.ib_pool_id)
if old_pool.pool_joins.present?
expect(new_pool.post_group_joins.count).to eq(old_pool.pool_joins.count)
old_pool.pool_joins.each_with_index do |old_join, i|
new_join = new_pool.post_group_joins[i]
expect(new_join.post.ib_id).to eq(old_join.post.ib_post_id)
if old_join.left_post.present?
expect(new_join.left_post.ib_id).to eq(old_join.left_post.ib_post_id)
else
expect(new_join.left_post).to be_nil
end
if old_join.right_post.present?
expect(new_join.right_post.ib_id).to eq(
old_join.right_post.ib_post_id,
)
else
expect(new_join.right_post).to be_nil
end
end
else
expect(new_pool.post_group_joins).to be_empty
end
end
describe "#migrate_inkbunny_users" do
let!(:old_user) do
create(
:domain_inkbunny_user,
name: "test_user",
ib_user_id: 123,
state: :ok,
state_detail: {
},
scanned_gallery_at: Time.current,
deep_update_log_entry: create(:http_log_entry),
shallow_update_log_entry: create(:http_log_entry),
)
end
it "migrates users that don't exist in the new table" do
expect { migrator.migrate_inkbunny_users }.to change(
Domain::User::InkbunnyUser,
:count,
).by(1)
new_user = Domain::User::InkbunnyUser.find_by(ib_id: old_user.ib_user_id)
expect_inkbunny_users_match(old_user, new_user)
end
it "skips users that already exist in the new table" do
# Create a user in the new table first
Domain::User::InkbunnyUser.create!(
ib_id: old_user.ib_user_id,
name: old_user.name,
state: "ok",
)
expect { migrator.migrate_inkbunny_users }.not_to change(
Domain::User::InkbunnyUser,
:count,
)
end
it "handles multiple users in batches" do
# Create a few more old users
additional_users =
2.times.map do |i|
create(
:domain_inkbunny_user,
name: "test_user_#{i}",
ib_user_id: 456 + i,
scanned_gallery_at: Time.current,
deep_update_log_entry: create(:http_log_entry),
shallow_update_log_entry: create(:http_log_entry),
)
end
expect { migrator.migrate_inkbunny_users }.to change(
Domain::User::InkbunnyUser,
:count,
).by(3)
expect(Domain::User::InkbunnyUser.count).to eq(3)
expect(Domain::User::InkbunnyUser.pluck(:ib_id)).to contain_exactly(
123,
456,
457,
)
# Verify all users were migrated correctly
([old_user] + additional_users).each do |old_user|
new_user =
Domain::User::InkbunnyUser.find_by(ib_id: old_user.ib_user_id)
expect_inkbunny_users_match(old_user, new_user)
end
end
it "handles users with avatars" do
avatar_log_entry =
create(
:http_log_entry,
uri_str: "https://example.com/avatar.jpg",
status_code: 200,
)
old_user.avatar_log_entry = avatar_log_entry
old_user.save!
migrator.migrate_inkbunny_users
new_user = Domain::User::InkbunnyUser.find_by(ib_id: old_user.ib_user_id)
expect_inkbunny_users_match(old_user, new_user)
expect(new_user.avatar).to be_present
expect(new_user.avatar.url_str).to eq(avatar_log_entry.uri_str)
expect(new_user.avatar.state).to eq("ok")
end
it "handles users with errored avatars" do
avatar_log_entry =
create(
:http_log_entry,
uri_str: "https://example.com/avatar.jpg",
status_code: 404,
)
old_user.avatar_log_entry = avatar_log_entry
old_user.save!
migrator.migrate_inkbunny_users
new_user = Domain::User::InkbunnyUser.find_by(ib_id: old_user.ib_user_id)
expect_inkbunny_users_match(old_user, new_user)
expect(new_user.avatar).to be_present
expect(new_user.avatar.url_str).to eq(avatar_log_entry.uri_str)
expect(new_user.avatar.state).to eq("error")
end
end
describe "#migrate_inkbunny_posts" do
let!(:creator) do
Domain::Inkbunny::User.create!(
name: "artist1",
ib_user_id: 123,
state: :ok,
state_detail: {
},
)
end
let!(:new_creator) do
Domain::User::InkbunnyUser.create!(
name: "artist1",
ib_id: 123,
state: "ok",
)
end
let!(:old_post) do
post = create(:domain_inkbunny_post, creator: creator)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
it "migrates posts that don't exist in the new table" do
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(1)
new_post = Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
expect_inkbunny_posts_match(old_post, new_post)
end
it "skips posts that already exist in the new table" do
# Create a post in the new table first
Domain::Post::InkbunnyPost.create!(
ib_id: old_post.ib_post_id,
state: "ok",
rating: "general",
submission_type: "picture_pinup",
)
expect { migrator.migrate_inkbunny_posts }.not_to change(
Domain::Post::InkbunnyPost,
:count,
)
end
it "handles multiple posts in batches" do
# Create a few more old posts
additional_posts =
2.times.map do |i|
post = create(:domain_inkbunny_post, creator: creator)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(3)
expect(Domain::Post::InkbunnyPost.count).to eq(3)
expect(Domain::Post::InkbunnyPost.pluck(:ib_id)).to contain_exactly(
old_post.ib_post_id,
additional_posts[0].ib_post_id,
additional_posts[1].ib_post_id,
)
# Verify all posts were migrated correctly
([old_post] + additional_posts).each do |old_post|
new_post =
Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
expect_inkbunny_posts_match(old_post, new_post)
end
end
it "handles posts without files" do
post_without_files =
Domain::Inkbunny::Post.create!(
ib_post_id: 999,
state: :ok,
state_detail: {
},
rating: :general,
submission_type: :picture_pinup,
creator: creator,
)
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(2)
new_post =
Domain::Post::InkbunnyPost.find_by(ib_id: post_without_files.ib_post_id)
expect_inkbunny_posts_match(post_without_files, new_post)
end
end
describe "#migrate_inkbunny_pools" do
let!(:creator) do
Domain::Inkbunny::User.create!(
name: "artist1",
ib_user_id: 123,
state: :ok,
state_detail: {
},
)
end
let!(:new_creator) do
Domain::User::InkbunnyUser.create!(
name: "artist1",
ib_id: 123,
state: "ok",
)
end
let!(:old_post1) do
post = create(:domain_inkbunny_post, creator: creator, ib_post_id: 1)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
let!(:old_post2) do
post = create(:domain_inkbunny_post, creator: creator, ib_post_id: 2)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
let!(:old_post3) do
post = create(:domain_inkbunny_post, creator: creator, ib_post_id: 3)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
let!(:new_post1) do
Domain::Post::InkbunnyPost.create!(
ib_id: old_post1.ib_post_id,
state: "ok",
rating: "general",
submission_type: "picture_pinup",
)
end
let!(:new_post2) do
Domain::Post::InkbunnyPost.create!(
ib_id: old_post2.ib_post_id,
state: "ok",
rating: "general",
submission_type: "picture_pinup",
)
end
let!(:new_post3) do
Domain::Post::InkbunnyPost.create!(
ib_id: old_post3.ib_post_id,
state: "ok",
rating: "general",
submission_type: "picture_pinup",
)
end
let!(:old_pool) do
pool = create(:domain_inkbunny_pool, ib_pool_id: 123)
# Create pool joins with sequential posts
pool.pool_joins.create!(
post: old_post1,
left_post: nil,
right_post: old_post2,
)
pool.pool_joins.create!(
post: old_post2,
left_post: old_post1,
right_post: old_post3,
)
pool.pool_joins.create!(
post: old_post3,
left_post: old_post2,
right_post: nil,
)
pool
end
it "migrates pools that don't exist in the new table" do
expect { migrator.migrate_inkbunny_pools }.to change(
Domain::PostGroup::InkbunnyPool,
:count,
).by(1)
new_pool =
Domain::PostGroup::InkbunnyPool.find_by(ib_id: old_pool.ib_pool_id)
expect_inkbunny_pools_match(old_pool, new_pool)
end
it "skips pools that already exist in the new table" do
# Create a pool in the new table first
Domain::PostGroup::InkbunnyPool.create!(ib_id: old_pool.ib_pool_id)
expect { migrator.migrate_inkbunny_pools }.not_to change(
Domain::PostGroup::InkbunnyPool,
:count,
)
end
it "handles multiple pools in batches" do
# Create a few more old pools
additional_pools =
2.times.map do |i|
pool = create(:domain_inkbunny_pool, ib_pool_id: 456 + i)
# Add some joins to each pool
pool.pool_joins.create!(
post: old_post1,
left_post: nil,
right_post: old_post2,
)
pool.pool_joins.create!(
post: old_post2,
left_post: old_post1,
right_post: nil,
)
pool
end
expect { migrator.migrate_inkbunny_pools }.to change(
Domain::PostGroup::InkbunnyPool,
:count,
).by(3)
expect(Domain::PostGroup::InkbunnyPool.count).to eq(3)
expect(Domain::PostGroup::InkbunnyPool.pluck(:ib_id)).to contain_exactly(
123,
456,
457,
)
# Verify all pools were migrated correctly
([old_pool] + additional_pools).each do |old_pool|
new_pool =
Domain::PostGroup::InkbunnyPool.find_by(ib_id: old_pool.ib_pool_id)
expect_inkbunny_pools_match(old_pool, new_pool)
end
end
it "handles pools without joins" do
pool_without_joins = create(:domain_inkbunny_pool, ib_pool_id: 789)
expect { migrator.migrate_inkbunny_pools }.to change(
Domain::PostGroup::InkbunnyPool,
:count,
).by(2)
new_pool =
Domain::PostGroup::InkbunnyPool.find_by(
ib_id: pool_without_joins.ib_pool_id,
)
expect_inkbunny_pools_match(pool_without_joins, new_pool)
end
it "maintains the correct post relationships in pool joins" do
migrator.migrate_inkbunny_pools
new_pool =
Domain::PostGroup::InkbunnyPool.find_by(ib_id: old_pool.ib_pool_id)
# Verify the sequence of posts is maintained
joins = new_pool.post_group_joins.sort_by { |j| j.post.ib_id }
# First join should link to second post
expect(joins[0].post.ib_id).to eq(old_post1.ib_post_id)
expect(joins[0].left_post).to be_nil
expect(joins[0].right_post.ib_id).to eq(old_post2.ib_post_id)
# Middle join should link to both first and third posts
expect(joins[1].post.ib_id).to eq(old_post2.ib_post_id)
expect(joins[1].left_post.ib_id).to eq(old_post1.ib_post_id)
expect(joins[1].right_post.ib_id).to eq(old_post3.ib_post_id)
# Last join should link to second post
expect(joins[2].post.ib_id).to eq(old_post3.ib_post_id)
expect(joins[2].left_post.ib_id).to eq(old_post2.ib_post_id)
expect(joins[2].right_post).to be_nil
end
end
end

View File

@@ -0,0 +1,7 @@
# typed: false
require "rails_helper"
RSpec.shared_context "migrate_to_domain" do
# sink to /dev/null
let(:migrator) { Domain::MigrateToDomain.new(File.open("/dev/null", "w")) }
end

File diff suppressed because it is too large Load Diff