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:) tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip) Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) { |t| t.category = 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("")) 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 にあった古い nico tag は active から外され、履歴として discard される" 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") old_pt = PostTag.create!(post: post, tag: old_nico) expect(old_pt.discarded_at).to be_nil # 今回は 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("")) run_rake_task("nico:sync") # OLD は active から外れる(discarded_at が入る) old_pts = PostTag.where(post_id: post.id, tag_id: old_nico.id).order(:id).to_a expect(old_pts.last.discarded_at).to be_present # 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), parent: post.parent, 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('')) 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('')) 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('')) 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 end