ファイル
btrc-hub/backend/spec/services/post_bulk_creator_spec.rb
T
2026-11-03 13:37:09 +09:00

174 行
6.6 KiB
Ruby

require 'rails_helper'
RSpec.describe PostBulkCreator do
include_context 'Ainu locale'
it 'limits workers to two and keeps failures in their request slots' do
locale = Locale.nipponese
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123) {
instance_double(User, id: 123)
}
mutex = Mutex.new
active = 0
maximum_active = 0
creators = []
allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
preflight = instance_double(PostCreatePreflight)
allow(preflight).to receive(:run) do
mutex.synchronize do
active += 1
maximum_active = [maximum_active, active].max
end
sleep 0.02
mutex.synchronize { active -= 1 }
attributes.symbolize_keys.merge(
existing_post_id: nil,
field_warnings: { },
base_warnings: [])
end
preflight
end
allow(PostCreator).to receive(:new) do |attributes:, **|
creator = instance_double(PostCreator)
mutex.synchronize { creators << creator }
if attributes[:title] == 'broken'
allow(creator).to receive(:create!).and_raise(StandardError, 'broken')
else
post = instance_double(Post, id: attributes[:title].delete_prefix('post ').to_i)
allow(creator).to receive(:create!).and_return(post)
end
creator
end
posts = [
{ 'title' => 'post 1', 'url' => 'https://example.com/1' },
{ 'title' => 'broken', 'url' => 'https://example.com/2' },
{ 'title' => 'post 3', 'url' => 'https://example.com/3' },
{ 'title' => 'post 4', 'url' => 'https://example.com/4' }]
results = described_class.new(
actor:,
posts:,
thumbnails: { }).run(locale).fetch(:results)
expect(creators.length).to eq(posts.length)
creators.each do |creator|
expect(creator).to have_received(:create!).with(locale).once
end
expect(maximum_active).to eq(2)
expect(results.length).to eq(posts.length)
expect(results.map { _1[:status] }).to eq(
['created', 'failed', 'created', 'created'])
expect(results[0].dig(:post, :id)).to eq(1)
expect(results[1]).to include(status: 'failed', recoverable: false)
expect(results[2].dig(:post, :id)).to eq(3)
expect(results[3].dig(:post, :id)).to eq(4)
end
it 'passes a future Ainu locale to preflight and creation in every worker' do
locale
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123).and_return(actor)
preflight = instance_double(PostCreatePreflight)
creator = instance_double(PostCreator)
post = instance_double(Post, id: 1)
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
allow(preflight).to receive(:run).with(locale).and_return(existing_post_id: nil)
allow(PostCreator).to receive(:new).and_return(creator)
allow(creator).to receive(:create!).with(locale).and_return(post)
results = described_class.new(
actor:, posts: [{ title: 'future post' }], thumbnails: { }).run(locale)
expect(results.fetch(:results)).to contain_exactly(
hash_including(status: 'created', post: { id: 1 }))
expect(preflight).to have_received(:run).with(locale).once
expect(creator).to have_received(:create!).with(locale).once
end
end
RSpec.describe 'PostBulkCreator event grouping', type: :model do
self.use_transactional_tests = false
before do
expect(Rails.env.test?).to be(true)
expect(ApplicationRecord.connection.open_transactions).to eq(0)
@prefix = "bulk_event_#{ SecureRandom.hex(8) }"
@actor = create(:user, :member)
allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
specs = [{ name: attributes.fetch(:title), category: :general }]
instance_double(PostCreatePreflight, run: attributes.merge(
existing_post_id: nil, snapshot_tag_specs: specs, post_tag_specs: specs,
normalised_parent_post_ids: [], tag_sections: { }, video_ms: nil))
end
end
after do
if @actor
posts = Post.where(uploaded_user: @actor)
PostTagSection.where(post_id: posts.select(:id)).delete_all
PostTag.where(post_id: posts.select(:id)).delete_all
PostVersion.where(post_id: posts.select(:id)).delete_all
posts.delete_all
tag_ids = TagName.where(name: ["#{ @prefix }_1", "#{ @prefix }_2"]).pluck(:tag_id)
TagVersion.where(tag_id: tag_ids).delete_all
TagName.where(tag_id: tag_ids).delete_all
Tag.where(id: tag_ids).delete_all
EventRecords::EventRow.where(created_by_user_id: @actor.id).delete_all
@actor.delete
end
end
it 'shares one creation event between real workers and their tag histories' do
posts = [1, 2].map do |number|
{ title: "#{ @prefix }_#{ number }",
url: "https://example.com/#{ @prefix }/#{ number }" }
end
result = nil
expect {
result = PostBulkCreator.new(actor: @actor, posts:, thumbnails: { })
.run(Locale.nipponese)
}.to change { event_rows.length }.by(1)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['created', 'created'])
event = event_rows.last
expect(event).to include(
'event_type' => 'posts.create', 'created_by_user_id' => @actor.id)
versions = PostVersion.where(created_by_user_id: @actor.id)
expect(versions.count).to eq(2)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
tag_versions = TagVersion.where(created_by_user_id: @actor.id)
expect(tag_versions.count).to eq(2)
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end
it 'does not leave an event when every row fails validation' do
preflight = instance_double(PostCreatePreflight)
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
allow(preflight).to receive(:run).and_raise(
PostCreatePreflight::ValidationFailed.new(fields: { url: ['invalid'] }))
events = event_rows
result = PostBulkCreator.new(
actor: @actor, posts: [{ title: 'invalid' }], thumbnails: { }).run(Locale.nipponese)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['failed'])
expect(event_rows).to eq(events)
end
it 'does not leave an event when every row is skipped' do
preflight = instance_double(PostCreatePreflight, run: { existing_post_id: 123 })
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
events = event_rows
result = PostBulkCreator.new(
actor: @actor, posts: [{ title: 'existing' }], thumbnails: { }).run(Locale.nipponese)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['skipped'])
expect(event_rows).to eq(events)
end
end