|
|
|
@@ -0,0 +1,310 @@ |
|
|
|
require 'rails_helper' |
|
|
|
|
|
|
|
RSpec.describe Youtube::Sync do |
|
|
|
let(:client) { instance_double(Youtube::ApiClient) } |
|
|
|
let(:sync) { described_class.new(client:) } |
|
|
|
|
|
|
|
before do |
|
|
|
allow(PostVersionRecorder).to receive(:record!) |
|
|
|
allow(PostVersionRecorder).to receive(:ensure_snapshot!) |
|
|
|
allow(sync).to receive(:attach_thumbnail_if_needed!) |
|
|
|
end |
|
|
|
|
|
|
|
describe '#sync!' do |
|
|
|
it 'returns without fetching video details when no video ids are discovered' do |
|
|
|
allow(sync).to receive(:query_terms).and_return([]) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return([]) |
|
|
|
|
|
|
|
expect(client).not_to receive(:videos) |
|
|
|
|
|
|
|
sync.sync! |
|
|
|
end |
|
|
|
|
|
|
|
it 'discovers ids from search and all playlist pages' do |
|
|
|
allow(sync).to receive(:query_terms).and_return(['ぼざろクリーチャー']) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return(['PL123']) |
|
|
|
allow(sync).to receive(:sync_since).and_return(Time.zone.parse('2026-05-01 00:00:00')) |
|
|
|
|
|
|
|
allow(client).to receive(:search_videos).with( |
|
|
|
q: 'ぼざろクリーチャー', |
|
|
|
published_after: Time.zone.parse('2026-05-01 00:00:00') |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'id' => { |
|
|
|
'videoId' => 'search-video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: nil |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'contentDetails' => { |
|
|
|
'videoId' => 'playlist-video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
], |
|
|
|
'nextPageToken' => 'NEXT' |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: 'NEXT' |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'snippet' => { |
|
|
|
'resourceId' => { |
|
|
|
'videoId' => 'playlist-video-2' |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
expect(client).to receive(:videos).with( |
|
|
|
satisfy do |ids| |
|
|
|
ids.sort == ['playlist-video-1', 'playlist-video-2', 'search-video-1'] |
|
|
|
end |
|
|
|
).and_return({ 'items' => [] }) |
|
|
|
|
|
|
|
sync.sync! |
|
|
|
end |
|
|
|
|
|
|
|
it 'creates a YouTube post with default tags and no_deerjikist when no deerjikist mapping exists' do |
|
|
|
Tag.tagme |
|
|
|
Tag.bot |
|
|
|
Tag.youtube |
|
|
|
Tag.video |
|
|
|
Tag.no_deerjikist |
|
|
|
|
|
|
|
allow(sync).to receive(:query_terms).and_return([]) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return(['PL123']) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: nil |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'contentDetails' => { |
|
|
|
'videoId' => 'video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:videos).with(['video-1']).and_return({ |
|
|
|
'items' => [ |
|
|
|
youtube_video_item( |
|
|
|
id: 'video-1', |
|
|
|
title: 'YouTube テスト動画', |
|
|
|
channel_id: 'UC_NO_MAPPING' |
|
|
|
) |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
expect do |
|
|
|
sync.sync! |
|
|
|
end.to change(Post, :count).by(1) |
|
|
|
|
|
|
|
post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1') |
|
|
|
tag_ids = post.tags.pluck(:id) |
|
|
|
|
|
|
|
expect(post.title).to eq('YouTube テスト動画') |
|
|
|
expect(post.uploaded_user_id).to be_nil |
|
|
|
expect(post.original_created_from).to eq(Time.zone.parse('2026-05-01 12:34:00')) |
|
|
|
expect(post.original_created_before).to eq(Time.zone.parse('2026-05-01 12:35:00')) |
|
|
|
|
|
|
|
expect(tag_ids).to include(Tag.tagme.id) |
|
|
|
expect(tag_ids).to include(Tag.bot.id) |
|
|
|
expect(tag_ids).to include(Tag.youtube.id) |
|
|
|
expect(tag_ids).to include(Tag.video.id) |
|
|
|
expect(tag_ids).to include(Tag.no_deerjikist.id) |
|
|
|
|
|
|
|
expect(PostVersionRecorder).to have_received(:record!).with( |
|
|
|
post:, |
|
|
|
event_type: :create, |
|
|
|
created_by_user: nil |
|
|
|
) |
|
|
|
end |
|
|
|
|
|
|
|
it 'uses deerjikist tag when channel id is mapped' do |
|
|
|
Tag.tagme |
|
|
|
Tag.bot |
|
|
|
Tag.youtube |
|
|
|
Tag.video |
|
|
|
Tag.no_deerjikist |
|
|
|
|
|
|
|
deerjikist_tag = Tag.find_or_create_by_tag_name!('テスト投稿者', category: :deerjikist) |
|
|
|
Deerjikist.create!( |
|
|
|
platform: 'youtube', |
|
|
|
code: 'UC_MAPPED', |
|
|
|
tag: deerjikist_tag |
|
|
|
) |
|
|
|
|
|
|
|
allow(sync).to receive(:query_terms).and_return([]) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return(['PL123']) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: nil |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'contentDetails' => { |
|
|
|
'videoId' => 'video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:videos).with(['video-1']).and_return({ |
|
|
|
'items' => [ |
|
|
|
youtube_video_item( |
|
|
|
id: 'video-1', |
|
|
|
title: 'YouTube テスト動画', |
|
|
|
channel_id: 'UC_MAPPED' |
|
|
|
) |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
sync.sync! |
|
|
|
|
|
|
|
post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1') |
|
|
|
tag_ids = post.tags.pluck(:id) |
|
|
|
|
|
|
|
expect(tag_ids).to include(deerjikist_tag.id) |
|
|
|
expect(tag_ids).not_to include(Tag.no_deerjikist.id) |
|
|
|
end |
|
|
|
|
|
|
|
it 'removes no_deerjikist when deerjikist mapping is added later' do |
|
|
|
Tag.no_deerjikist |
|
|
|
|
|
|
|
post = Post.create!( |
|
|
|
title: '旧タイトル', |
|
|
|
url: 'https://www.youtube.com/watch?v=video-1', |
|
|
|
uploaded_user_id: nil, |
|
|
|
original_created_from: Time.zone.parse('2026-05-01 00:00:00'), |
|
|
|
original_created_before: Time.zone.parse('2026-05-01 00:01:00') |
|
|
|
) |
|
|
|
PostTag.create!(post:, tag: Tag.no_deerjikist) |
|
|
|
|
|
|
|
deerjikist_tag = Tag.find_or_create_by_tag_name!('後から判明した投稿者', category: :deerjikist) |
|
|
|
Deerjikist.create!( |
|
|
|
platform: 'youtube', |
|
|
|
code: 'UC_MAPPED_LATER', |
|
|
|
tag: deerjikist_tag |
|
|
|
) |
|
|
|
|
|
|
|
allow(sync).to receive(:query_terms).and_return([]) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return(['PL123']) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: nil |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'contentDetails' => { |
|
|
|
'videoId' => 'video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:videos).with(['video-1']).and_return({ |
|
|
|
'items' => [ |
|
|
|
youtube_video_item( |
|
|
|
id: 'video-1', |
|
|
|
title: '新タイトル', |
|
|
|
channel_id: 'UC_MAPPED_LATER' |
|
|
|
) |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
sync.sync! |
|
|
|
|
|
|
|
post.reload |
|
|
|
tag_ids = post.tags.pluck(:id) |
|
|
|
|
|
|
|
expect(post.title).to eq('新タイトル') |
|
|
|
expect(tag_ids).to include(deerjikist_tag.id) |
|
|
|
expect(tag_ids).not_to include(Tag.no_deerjikist.id) |
|
|
|
|
|
|
|
expect(PostVersionRecorder).to have_received(:ensure_snapshot!).with( |
|
|
|
post, |
|
|
|
created_by_user: nil |
|
|
|
) |
|
|
|
expect(PostVersionRecorder).to have_received(:record!).with( |
|
|
|
post:, |
|
|
|
event_type: :update, |
|
|
|
created_by_user: nil |
|
|
|
) |
|
|
|
end |
|
|
|
|
|
|
|
it 'matches existing youtu.be URL and does not create duplicate post' do |
|
|
|
post = Post.create!( |
|
|
|
title: '旧タイトル', |
|
|
|
url: 'https://youtu.be/video-1', |
|
|
|
uploaded_user_id: nil, |
|
|
|
original_created_from: Time.zone.parse('2026-05-01 00:00:00'), |
|
|
|
original_created_before: Time.zone.parse('2026-05-01 00:01:00') |
|
|
|
) |
|
|
|
|
|
|
|
allow(sync).to receive(:query_terms).and_return([]) |
|
|
|
allow(sync).to receive(:playlist_ids).and_return(['PL123']) |
|
|
|
|
|
|
|
allow(client).to receive(:playlist_items).with( |
|
|
|
playlist_id: 'PL123', |
|
|
|
page_token: nil |
|
|
|
).and_return({ |
|
|
|
'items' => [ |
|
|
|
{ |
|
|
|
'contentDetails' => { |
|
|
|
'videoId' => 'video-1' |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
allow(client).to receive(:videos).with(['video-1']).and_return({ |
|
|
|
'items' => [ |
|
|
|
youtube_video_item( |
|
|
|
id: 'video-1', |
|
|
|
title: '新タイトル', |
|
|
|
channel_id: 'UC_NO_MAPPING' |
|
|
|
) |
|
|
|
] |
|
|
|
}) |
|
|
|
|
|
|
|
expect do |
|
|
|
sync.sync! |
|
|
|
end.not_to change(Post, :count) |
|
|
|
|
|
|
|
expect(post.reload.title).to eq('新タイトル') |
|
|
|
end |
|
|
|
end |
|
|
|
|
|
|
|
def youtube_video_item(id:, title:, channel_id:) |
|
|
|
{ |
|
|
|
'id' => id, |
|
|
|
'snippet' => { |
|
|
|
'title' => title, |
|
|
|
'channelId' => channel_id, |
|
|
|
'publishedAt' => '2026-05-01T12:34:56Z', |
|
|
|
'thumbnails' => { |
|
|
|
'high' => { |
|
|
|
'url' => "https://img.youtube.com/#{id}.jpg" |
|
|
|
} |
|
|
|
}, |
|
|
|
'tags' => ['tag-a', 'tag-b'] |
|
|
|
} |
|
|
|
} |
|
|
|
end |
|
|
|
end |