ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

311 lines
8.4 KiB

  1. require 'rails_helper'
  2. RSpec.describe Youtube::Sync do
  3. let(:client) { instance_double(Youtube::ApiClient) }
  4. let(:sync) { described_class.new(client:) }
  5. before do
  6. allow(PostVersionRecorder).to receive(:record!)
  7. allow(PostVersionRecorder).to receive(:ensure_snapshot!)
  8. allow(sync).to receive(:attach_thumbnail_if_needed!)
  9. end
  10. describe '#sync!' do
  11. it 'returns without fetching video details when no video ids are discovered' do
  12. allow(sync).to receive(:query_terms).and_return([])
  13. allow(sync).to receive(:playlist_ids).and_return([])
  14. expect(client).not_to receive(:videos)
  15. sync.sync!
  16. end
  17. it 'discovers ids from search and all playlist pages' do
  18. allow(sync).to receive(:query_terms).and_return(['ぼざろクリーチャー'])
  19. allow(sync).to receive(:playlist_ids).and_return(['PL123'])
  20. allow(sync).to receive(:sync_since).and_return(Time.zone.parse('2026-05-01 00:00:00'))
  21. allow(client).to receive(:search_videos).with(
  22. q: 'ぼざろクリーチャー',
  23. published_after: Time.zone.parse('2026-05-01 00:00:00')
  24. ).and_return({
  25. 'items' => [
  26. {
  27. 'id' => {
  28. 'videoId' => 'search-video-1'
  29. }
  30. }
  31. ]
  32. })
  33. allow(client).to receive(:playlist_items).with(
  34. playlist_id: 'PL123',
  35. page_token: nil
  36. ).and_return({
  37. 'items' => [
  38. {
  39. 'contentDetails' => {
  40. 'videoId' => 'playlist-video-1'
  41. }
  42. }
  43. ],
  44. 'nextPageToken' => 'NEXT'
  45. })
  46. allow(client).to receive(:playlist_items).with(
  47. playlist_id: 'PL123',
  48. page_token: 'NEXT'
  49. ).and_return({
  50. 'items' => [
  51. {
  52. 'snippet' => {
  53. 'resourceId' => {
  54. 'videoId' => 'playlist-video-2'
  55. }
  56. }
  57. }
  58. ]
  59. })
  60. expect(client).to receive(:videos).with(
  61. satisfy do |ids|
  62. ids.sort == ['playlist-video-1', 'playlist-video-2', 'search-video-1']
  63. end
  64. ).and_return({ 'items' => [] })
  65. sync.sync!
  66. end
  67. it 'creates a YouTube post with default tags and no_deerjikist when no deerjikist mapping exists' do
  68. Tag.tagme
  69. Tag.bot
  70. Tag.youtube
  71. Tag.video
  72. Tag.no_deerjikist
  73. allow(sync).to receive(:query_terms).and_return([])
  74. allow(sync).to receive(:playlist_ids).and_return(['PL123'])
  75. allow(client).to receive(:playlist_items).with(
  76. playlist_id: 'PL123',
  77. page_token: nil
  78. ).and_return({
  79. 'items' => [
  80. {
  81. 'contentDetails' => {
  82. 'videoId' => 'video-1'
  83. }
  84. }
  85. ]
  86. })
  87. allow(client).to receive(:videos).with(['video-1']).and_return({
  88. 'items' => [
  89. youtube_video_item(
  90. id: 'video-1',
  91. title: 'YouTube テスト動画',
  92. channel_id: 'UC_NO_MAPPING'
  93. )
  94. ]
  95. })
  96. expect do
  97. sync.sync!
  98. end.to change(Post, :count).by(1)
  99. post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1')
  100. tag_ids = post.tags.pluck(:id)
  101. expect(post.title).to eq('YouTube テスト動画')
  102. expect(post.uploaded_user_id).to be_nil
  103. expect(post.original_created_from).to eq(Time.zone.parse('2026-05-01 12:34:00'))
  104. expect(post.original_created_before).to eq(Time.zone.parse('2026-05-01 12:35:00'))
  105. expect(tag_ids).to include(Tag.tagme.id)
  106. expect(tag_ids).to include(Tag.bot.id)
  107. expect(tag_ids).to include(Tag.youtube.id)
  108. expect(tag_ids).to include(Tag.video.id)
  109. expect(tag_ids).to include(Tag.no_deerjikist.id)
  110. expect(PostVersionRecorder).to have_received(:record!).with(
  111. post:,
  112. event_type: :create,
  113. created_by_user: nil
  114. )
  115. end
  116. it 'uses deerjikist tag when channel id is mapped' do
  117. Tag.tagme
  118. Tag.bot
  119. Tag.youtube
  120. Tag.video
  121. Tag.no_deerjikist
  122. deerjikist_tag = Tag.find_or_create_by_tag_name!('テスト投稿者', category: :deerjikist)
  123. Deerjikist.create!(
  124. platform: 'youtube',
  125. code: 'UC_MAPPED',
  126. tag: deerjikist_tag
  127. )
  128. allow(sync).to receive(:query_terms).and_return([])
  129. allow(sync).to receive(:playlist_ids).and_return(['PL123'])
  130. allow(client).to receive(:playlist_items).with(
  131. playlist_id: 'PL123',
  132. page_token: nil
  133. ).and_return({
  134. 'items' => [
  135. {
  136. 'contentDetails' => {
  137. 'videoId' => 'video-1'
  138. }
  139. }
  140. ]
  141. })
  142. allow(client).to receive(:videos).with(['video-1']).and_return({
  143. 'items' => [
  144. youtube_video_item(
  145. id: 'video-1',
  146. title: 'YouTube テスト動画',
  147. channel_id: 'UC_MAPPED'
  148. )
  149. ]
  150. })
  151. sync.sync!
  152. post = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-1')
  153. tag_ids = post.tags.pluck(:id)
  154. expect(tag_ids).to include(deerjikist_tag.id)
  155. expect(tag_ids).not_to include(Tag.no_deerjikist.id)
  156. end
  157. it 'removes no_deerjikist when deerjikist mapping is added later' do
  158. Tag.no_deerjikist
  159. post = Post.create!(
  160. title: '旧タイトル',
  161. url: 'https://www.youtube.com/watch?v=video-1',
  162. uploaded_user_id: nil,
  163. original_created_from: Time.zone.parse('2026-05-01 00:00:00'),
  164. original_created_before: Time.zone.parse('2026-05-01 00:01:00')
  165. )
  166. PostTag.create!(post:, tag: Tag.no_deerjikist)
  167. deerjikist_tag = Tag.find_or_create_by_tag_name!('後から判明した投稿者', category: :deerjikist)
  168. Deerjikist.create!(
  169. platform: 'youtube',
  170. code: 'UC_MAPPED_LATER',
  171. tag: deerjikist_tag
  172. )
  173. allow(sync).to receive(:query_terms).and_return([])
  174. allow(sync).to receive(:playlist_ids).and_return(['PL123'])
  175. allow(client).to receive(:playlist_items).with(
  176. playlist_id: 'PL123',
  177. page_token: nil
  178. ).and_return({
  179. 'items' => [
  180. {
  181. 'contentDetails' => {
  182. 'videoId' => 'video-1'
  183. }
  184. }
  185. ]
  186. })
  187. allow(client).to receive(:videos).with(['video-1']).and_return({
  188. 'items' => [
  189. youtube_video_item(
  190. id: 'video-1',
  191. title: '新タイトル',
  192. channel_id: 'UC_MAPPED_LATER'
  193. )
  194. ]
  195. })
  196. sync.sync!
  197. post.reload
  198. tag_ids = post.tags.pluck(:id)
  199. expect(post.title).to eq('新タイトル')
  200. expect(tag_ids).to include(deerjikist_tag.id)
  201. expect(tag_ids).not_to include(Tag.no_deerjikist.id)
  202. expect(PostVersionRecorder).to have_received(:ensure_snapshot!).with(
  203. post,
  204. created_by_user: nil
  205. )
  206. expect(PostVersionRecorder).to have_received(:record!).with(
  207. post:,
  208. event_type: :update,
  209. created_by_user: nil
  210. )
  211. end
  212. it 'matches existing youtu.be URL and does not create duplicate post' do
  213. post = Post.create!(
  214. title: '旧タイトル',
  215. url: 'https://youtu.be/video-1',
  216. uploaded_user_id: nil,
  217. original_created_from: Time.zone.parse('2026-05-01 00:00:00'),
  218. original_created_before: Time.zone.parse('2026-05-01 00:01:00')
  219. )
  220. allow(sync).to receive(:query_terms).and_return([])
  221. allow(sync).to receive(:playlist_ids).and_return(['PL123'])
  222. allow(client).to receive(:playlist_items).with(
  223. playlist_id: 'PL123',
  224. page_token: nil
  225. ).and_return({
  226. 'items' => [
  227. {
  228. 'contentDetails' => {
  229. 'videoId' => 'video-1'
  230. }
  231. }
  232. ]
  233. })
  234. allow(client).to receive(:videos).with(['video-1']).and_return({
  235. 'items' => [
  236. youtube_video_item(
  237. id: 'video-1',
  238. title: '新タイトル',
  239. channel_id: 'UC_NO_MAPPING'
  240. )
  241. ]
  242. })
  243. expect do
  244. sync.sync!
  245. end.not_to change(Post, :count)
  246. expect(post.reload.title).to eq('新タイトル')
  247. end
  248. end
  249. def youtube_video_item(id:, title:, channel_id:)
  250. {
  251. 'id' => id,
  252. 'snippet' => {
  253. 'title' => title,
  254. 'channelId' => channel_id,
  255. 'publishedAt' => '2026-05-01T12:34:56Z',
  256. 'thumbnails' => {
  257. 'high' => {
  258. 'url' => "https://img.youtube.com/#{id}.jpg"
  259. }
  260. },
  261. 'tags' => ['tag-a', 'tag-b']
  262. }
  263. }
  264. end
  265. end