|
- 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
|