mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
Add getCommentReplies method to YouTubeClient for fetching reply threads via the YouTube comments.list API. Modify CommentsSyncService to import reply threads during sync, storing them as type 'reply' with parentInteraction relationship in community-interactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
6.4 KiB
TypeScript
242 lines
6.4 KiB
TypeScript
/**
|
|
* YouTubeClient.getCommentReplies Unit Tests
|
|
*
|
|
* Tests the comment reply retrieval method that fetches replies
|
|
* for a given parent comment thread from the YouTube Data API.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const mockCommentsList = vi.fn()
|
|
|
|
vi.mock('googleapis', () => {
|
|
class MockOAuth2 {
|
|
setCredentials(): void {
|
|
// no-op
|
|
}
|
|
}
|
|
|
|
return {
|
|
google: {
|
|
auth: {
|
|
OAuth2: MockOAuth2,
|
|
},
|
|
youtube: () => ({
|
|
videos: { list: vi.fn() },
|
|
commentThreads: { list: vi.fn() },
|
|
comments: {
|
|
list: mockCommentsList,
|
|
insert: vi.fn(),
|
|
setModerationStatus: vi.fn(),
|
|
delete: vi.fn(),
|
|
},
|
|
channels: { list: vi.fn() },
|
|
}),
|
|
},
|
|
}
|
|
})
|
|
|
|
const mockPayload = {} as import('payload').Payload
|
|
|
|
describe('YouTubeClient.getCommentReplies', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should return parsed replies for a parent comment', async () => {
|
|
mockCommentsList.mockResolvedValueOnce({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 'reply-1',
|
|
snippet: {
|
|
parentId: 'comment-abc',
|
|
textOriginal: 'Great point!',
|
|
textDisplay: 'Great point!',
|
|
authorDisplayName: 'User A',
|
|
authorProfileImageUrl: 'https://example.com/a.jpg',
|
|
authorChannelUrl: 'https://youtube.com/channel/UC_a',
|
|
authorChannelId: { value: 'UC_a' },
|
|
likeCount: 5,
|
|
publishedAt: '2026-01-15T10:00:00Z',
|
|
updatedAt: '2026-01-15T10:00:00Z',
|
|
},
|
|
},
|
|
{
|
|
id: 'reply-2',
|
|
snippet: {
|
|
parentId: 'comment-abc',
|
|
textOriginal: 'Thanks for sharing',
|
|
textDisplay: 'Thanks for sharing',
|
|
authorDisplayName: 'User B',
|
|
authorProfileImageUrl: 'https://example.com/b.jpg',
|
|
authorChannelUrl: 'https://youtube.com/channel/UC_b',
|
|
authorChannelId: { value: 'UC_b' },
|
|
likeCount: 2,
|
|
publishedAt: '2026-01-15T11:00:00Z',
|
|
updatedAt: '2026-01-15T11:00:00Z',
|
|
},
|
|
},
|
|
],
|
|
nextPageToken: undefined,
|
|
},
|
|
})
|
|
|
|
const { YouTubeClient } = await import(
|
|
'@/lib/integrations/youtube/YouTubeClient'
|
|
)
|
|
|
|
const client = new YouTubeClient(
|
|
{
|
|
clientId: 'test-id',
|
|
clientSecret: 'test-secret',
|
|
accessToken: 'test-token',
|
|
refreshToken: 'test-refresh',
|
|
},
|
|
mockPayload,
|
|
)
|
|
|
|
const result = await client.getCommentReplies('comment-abc')
|
|
|
|
expect(result.replies).toHaveLength(2)
|
|
expect(result.replies[0].id).toBe('reply-1')
|
|
expect(result.replies[0].snippet.parentId).toBe('comment-abc')
|
|
expect(result.replies[0].snippet.textOriginal).toBe('Great point!')
|
|
expect(result.replies[0].snippet.likeCount).toBe(5)
|
|
expect(result.replies[1].id).toBe('reply-2')
|
|
expect(result.nextPageToken).toBeUndefined()
|
|
|
|
expect(mockCommentsList).toHaveBeenCalledWith({
|
|
part: ['snippet'],
|
|
parentId: 'comment-abc',
|
|
maxResults: 100,
|
|
textFormat: 'plainText',
|
|
})
|
|
})
|
|
|
|
it('should pass custom maxResults parameter', async () => {
|
|
mockCommentsList.mockResolvedValueOnce({
|
|
data: {
|
|
items: [],
|
|
nextPageToken: undefined,
|
|
},
|
|
})
|
|
|
|
const { YouTubeClient } = await import(
|
|
'@/lib/integrations/youtube/YouTubeClient'
|
|
)
|
|
|
|
const client = new YouTubeClient(
|
|
{
|
|
clientId: 'test-id',
|
|
clientSecret: 'test-secret',
|
|
accessToken: 'test-token',
|
|
refreshToken: 'test-refresh',
|
|
},
|
|
mockPayload,
|
|
)
|
|
|
|
await client.getCommentReplies('comment-xyz', 20)
|
|
|
|
expect(mockCommentsList).toHaveBeenCalledWith({
|
|
part: ['snippet'],
|
|
parentId: 'comment-xyz',
|
|
maxResults: 20,
|
|
textFormat: 'plainText',
|
|
})
|
|
})
|
|
|
|
it('should return empty replies array when no items exist', async () => {
|
|
mockCommentsList.mockResolvedValueOnce({
|
|
data: {
|
|
items: null,
|
|
nextPageToken: undefined,
|
|
},
|
|
})
|
|
|
|
const { YouTubeClient } = await import(
|
|
'@/lib/integrations/youtube/YouTubeClient'
|
|
)
|
|
|
|
const client = new YouTubeClient(
|
|
{
|
|
clientId: 'test-id',
|
|
clientSecret: 'test-secret',
|
|
accessToken: 'test-token',
|
|
refreshToken: 'test-refresh',
|
|
},
|
|
mockPayload,
|
|
)
|
|
|
|
const result = await client.getCommentReplies('comment-empty')
|
|
|
|
expect(result.replies).toEqual([])
|
|
expect(result.nextPageToken).toBeUndefined()
|
|
})
|
|
|
|
it('should return nextPageToken when more replies are available', async () => {
|
|
mockCommentsList.mockResolvedValueOnce({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 'reply-page1',
|
|
snippet: {
|
|
parentId: 'comment-many',
|
|
textOriginal: 'First page reply',
|
|
textDisplay: 'First page reply',
|
|
authorDisplayName: 'User C',
|
|
authorProfileImageUrl: 'https://example.com/c.jpg',
|
|
authorChannelUrl: 'https://youtube.com/channel/UC_c',
|
|
authorChannelId: { value: 'UC_c' },
|
|
likeCount: 0,
|
|
publishedAt: '2026-02-01T12:00:00Z',
|
|
updatedAt: '2026-02-01T12:00:00Z',
|
|
},
|
|
},
|
|
],
|
|
nextPageToken: 'page2token',
|
|
},
|
|
})
|
|
|
|
const { YouTubeClient } = await import(
|
|
'@/lib/integrations/youtube/YouTubeClient'
|
|
)
|
|
|
|
const client = new YouTubeClient(
|
|
{
|
|
clientId: 'test-id',
|
|
clientSecret: 'test-secret',
|
|
accessToken: 'test-token',
|
|
refreshToken: 'test-refresh',
|
|
},
|
|
mockPayload,
|
|
)
|
|
|
|
const result = await client.getCommentReplies('comment-many')
|
|
|
|
expect(result.replies).toHaveLength(1)
|
|
expect(result.nextPageToken).toBe('page2token')
|
|
})
|
|
|
|
it('should propagate API errors', async () => {
|
|
mockCommentsList.mockRejectedValueOnce(new Error('commentListForbidden'))
|
|
|
|
const { YouTubeClient } = await import(
|
|
'@/lib/integrations/youtube/YouTubeClient'
|
|
)
|
|
|
|
const client = new YouTubeClient(
|
|
{
|
|
clientId: 'test-id',
|
|
clientSecret: 'test-secret',
|
|
accessToken: 'test-token',
|
|
refreshToken: 'test-refresh',
|
|
},
|
|
mockPayload,
|
|
)
|
|
|
|
await expect(
|
|
client.getCommentReplies('comment-forbidden'),
|
|
).rejects.toThrow('commentListForbidden')
|
|
})
|
|
})
|