cms.c2sgmbh/tests/unit/youtube/comment-replies.unit.spec.ts
Martin Porwoll 289b69380f feat(youtube): add reply thread import to comment sync
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>
2026-02-14 13:28:33 +00:00

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')
})
})