ファイル
btrc-hub/backend/spec/tasks/nico_sync_spec.rb
T
2026-09-22 20:16:30 +09:00

497 行
15 KiB
Ruby

require 'rails_helper'
RSpec.describe 'nico:sync' do
def stub_python(json_array)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture3).and_return([json_array.to_json, '', status])
end
def create_tag!(name, category:)
Tag.find_or_create_by_tag_name!(name, category:)
end
def link_nico_to_tag!(nico_tag, tag)
NicoTagRelation.create!(nico_tag_id: nico_tag.id, tag_id: tag.id)
end
it '既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける' do
# 既存 post(正規表現で拾われるURL)
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
# 既存の非nicoタグ(kept_non_nico_ids)
kept_general = create_tag!('spec_kept', category: 'general')
PostTag.create!(post: post, tag: kept_general)
# 追加される linked tag を準備(nico tag に紐付く一般タグ)
linked = create_tag!('spec_linked', category: 'general')
nico = create_tag!('nico:AAA', category: 'nico')
link_nico_to_tag!(nico, linked)
# bot / tagme は task 内で使うので作っておく(Tag.bot/tagme がある前提)
Tag.bot
Tag.tagme
# pythonの出力(AAA が付く)
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56',
'deleted_at' => '2026-01-31 00:00:00' }])
# 外部HTTPは今回「既存 post なので呼ばれない」はずだが、念のため塞ぐ
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
run_rake_task('nico:sync')
post.reload
active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name')
expect(active_tag_names).to include('spec_kept')
expect(active_tag_names).to include('nico:AAA')
expect(active_tag_names).to include('spec_linked')
expect(post.original_created_from).to eq(Time.iso8601('2026-01-01T03:34:00Z'))
expect(post.original_created_before).to eq(Time.iso8601('2026-01-01T03:35:00Z'))
# 差分が出るので bot が付く(kept_non_nico_ids != desired_non_nico_ids)
expect(active_tag_names).to include('bot操作')
end
it '既存 post のサムネール取得に共通 attach 経路を使ふ' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil)
Tag.bot
Tag.tagme
stub_python([{ 'code' => 'sm9', 'title' => 't', 'tags' => [] }])
allow(URI).to receive(:open) do
StringIO.new(
'<meta name="thumbnail" content="https://example.com/thumb.jpg">')
end
expect_any_instance_of(Post).to receive(:attach_thumbnail_from_url!)
.with('https://example.com/thumb.jpg')
run_rake_task('nico:sync')
end
it 'サムネール取得失敗後も次回同期で再試行できる' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil)
Tag.bot
Tag.tagme
stub_python([{ 'code' => 'sm9', 'title' => 't', 'tags' => [] }])
allow(URI).to receive(:open) do
StringIO.new(
'<meta name="thumbnail" content="https://example.com/thumb.jpg">')
end
calls = 0
allow_any_instance_of(Post).to receive(:attach_thumbnail_from_url!) do
calls += 1
raise Post::RemoteThumbnailFetchFailed, 'failed'
end
2.times do
run_rake_task('nico:sync')
end
expect(calls).to eq(2)
end
it '古い nico tag の関連を物理削除し、変更前後の履歴を version に残す' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
# 旧nicoタグ(今回の同期結果に含まれない)
old_nico = create_tag!('nico:OLD', category: 'nico')
PostTag.create!(post:, tag: old_nico)
create_post_version_for!(post)
# 今回は NEW のみ欲しい
new_nico = create_tag!('nico:NEW', category: 'nico')
# bot/tagme 念のため
Tag.bot
Tag.tagme
stub_python([{ 'code' => 'sm9', 'title' => 't', 'tags' => ['NEW'] }])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
run_rake_task('nico:sync')
expect(PostTag.exists?(post:, tag: old_nico)).to be(false)
expect(old_nico.reload.post_count).to eq(0)
expect(new_nico.reload.post_count).to eq(1)
versions = post.post_versions.order(:version_no)
expect(versions.first.tags_json.map { |item| item.fetch('id') })
.to include(old_nico.id)
expect(versions.last.tags_json.map { |item| item.fetch('id') })
.to include(new_nico.id)
expect(versions.last.tags_json.map { |item| item.fetch('id') })
.not_to include(old_nico.id)
# NEW は active にいる
post.reload
active_names = post.tags.joins(:tag_name).pluck('tag_names.name')
expect(active_names).to include('nico:NEW')
expect(active_names).not_to include('nico:OLD')
end
def snapshot_tags(post)
post.snapshot_tag_names.join(' ')
end
def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil)
PostVersion.create!(
post: post,
version_no: version_no,
event_type: event_type,
title: post.title,
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
tags_json: post.snapshot_tags_json,
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
created_at: Time.current,
created_by_user: created_by_user
)
end
it '新規 post 作成時に version 1 を作る' do
Tag.bot
Tag.tagme
Tag.niconico
Tag.video
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change(PostVersion, :count).by(1)
post = Post.find_by!(url: 'https://www.nicovideo.jp/watch/sm9')
version = post.post_versions.order(:version_no).last
expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create')
expect(version.created_by_user).to be_nil
expect(version.tags).to eq(snapshot_tags(post.reload))
end
it '既存 post の内容または tags が変わったとき update version を作る' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
kept_general = create_tag!('spec_kept', category: 'general')
PostTag.create!(post: post, tag: kept_general)
create_post_version_for!(post)
linked = create_tag!('spec_linked', category: 'general')
nico = create_tag!('nico:AAA', category: 'nico')
link_nico_to_tag!(nico, linked)
Tag.bot
Tag.tagme
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change(PostVersion, :count).by(1)
version = post.reload.post_versions.order(:version_no).last
expect(version.version_no).to eq(2)
expect(version.event_type).to eq('update')
expect(version.created_by_user).to be_nil
expect(version.tags).to eq(snapshot_tags(post.reload))
end
it '既存 post に差分が無いときは新しい version を作らない' do
nico = create_tag!('nico:AAA', category: 'nico')
no_deerjikist = create_tag!('ニジラー情報不詳', category: 'meta')
post = Post.create!(
title: 't',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil,
original_created_from: Time.iso8601('2026-01-01T03:34:00Z'),
original_created_before: Time.iso8601('2026-01-01T03:35:00Z')
)
PostTag.create!(post: post, tag: nico)
PostTag.create!(post: post, tag: no_deerjikist)
create_post_version_for!(post)
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.not_to change(PostVersion, :count)
version = post.reload.post_versions.order(:version_no).last
expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create')
expect(version.tags).to eq(snapshot_tags(post.reload))
end
it '新規 nico tag に nico tag version を作る' do
Tag.bot
Tag.tagme
Tag.niconico
Tag.video
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change(NicoTagVersion, :count).by(1)
nico_tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'nico:AAA' })
version = nico_tag.nico_tag_versions.order(:version_no).last
expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create')
expect(version.name).to eq('nico:AAA')
expect(version.created_by_user).to be_nil
end
it '既存 post に version が無い場合は create snapshot を補う' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
kept_general = create_tag!('spec_kept_without_version', category: 'general')
PostTag.create!(post: post, tag: kept_general)
Tag.bot
Tag.tagme
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 'changed title',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change { post.reload.post_versions.count }.by(1)
versions = post.reload.post_versions.order(:version_no)
expect(versions.map(&:event_type)).to eq(['create'])
expect(versions.first.title).to eq('changed title')
expect(versions.first.tags).to eq(snapshot_tags(post.reload))
end
it '既存 version がある post には update version を作る' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
kept_general = create_tag!('spec_kept_with_version', category: 'general')
PostTag.create!(post: post, tag: kept_general)
PostVersionRecorder.record!(
post: post,
event_type: :create,
created_by_user: nil
)
Tag.bot
Tag.tagme
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 'changed title',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change { post.reload.post_versions.count }.by(1)
versions = post.reload.post_versions.order(:version_no)
expect(versions.map(&:event_type)).to eq(['create', 'update'])
expect(versions.first.title).to eq('old')
expect(versions.second.title).to eq('changed title')
expect(versions.second.tags).to eq(snapshot_tags(post.reload))
end
def create_external_tag!(name)
ExternalTag.create!(platform: :nico, name:)
end
def create_nico_sync_post!
post = Post.create!(
title: 't',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
PostTag.create!(post:, tag: Tag.no_deerjikist)
post
end
def run_nico_sync_with_tags! tags
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => tags,
'user' => nil
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
run_rake_task('nico:sync')
end
it '外部タグが新規記載されたとき,その時点の連携タグを記載する' do
post = create_nico_sync_post!
external_tag = create_external_tag!('AAA')
linked_tag = create_tag!('spec_linked', category: :general)
link_nico_to_tag!(external_tag, linked_tag)
run_nico_sync_with_tags!(['AAA'])
expect(post.reload.external_tags).to include(external_tag)
expect(post.tags).to include(linked_tag)
end
it '外部タグに差分がない場合,内外マッピングが変はっても連携タグを再評価しない' do
post = create_nico_sync_post!
external_tag = create_external_tag!('AAA')
old_linked_tag = create_tag!('spec_old_linked', category: :general)
new_linked_tag = create_tag!('spec_new_linked', category: :general)
relation = link_nico_to_tag!(external_tag, old_linked_tag)
run_nico_sync_with_tags!(['AAA'])
expect(post.reload.tags).to include(old_linked_tag)
# 人手で連携タグを消除する.
PostTag.find_by!(post:, tag: old_linked_tag).destroy!
# 内外マッピングを変更する.
relation.destroy!
link_nico_to_tag!(external_tag, new_linked_tag)
# 外部タグ自体には差分が無い.
run_nico_sync_with_tags!(['AAA'])
post.reload
expect(post.external_tags).to include(external_tag)
expect(post.tags).not_to include(old_linked_tag)
expect(post.tags).not_to include(new_linked_tag)
end
it '外部タグの消除では連携タグを消除せず,再記載時にその時点の内外マッピングを適用する' do
post = create_nico_sync_post!
external_tag = create_external_tag!('AAA')
old_linked_tag = create_tag!('spec_old_linked', category: :general)
new_linked_tag = create_tag!('spec_new_linked', category: :general)
relation = link_nico_to_tag!(external_tag, old_linked_tag)
# 外部タグを新規記載する.
run_nico_sync_with_tags!(['AAA'])
post.reload
expect(post.external_tags).to include(external_tag)
expect(post.tags).to include(old_linked_tag)
# 外部タグを消除する.
run_nico_sync_with_tags!([])
post.reload
expect(post.external_tags).not_to include(external_tag)
# 外部タグの消除によって連携タグまでは消除されない.
expect(post.tags).to include(old_linked_tag)
# 外部タグが記載されてゐない間に内外マッピングを変更する.
relation.destroy!
link_nico_to_tag!(external_tag, new_linked_tag)
# 同じ外部タグを再記載する.
run_nico_sync_with_tags!(['AAA'])
post.reload
expect(post.external_tags).to include(external_tag)
# 旧連携タグは自動的には消除されない.
expect(post.tags).to include(old_linked_tag)
# 再記載時点の内外マッピングが新たに適用される.
expect(post.tags).to include(new_linked_tag)
end
end