/** * VideoUploadService Unit Tests * * Tests the service that uploads videos to YouTube via the Data API v3. * Validates credential resolution, media file handling, and YouTube API interaction. */ import { describe, it, expect, vi, beforeEach } from 'vitest' // --- Mocks --- const mockVideosInsert = vi.fn() vi.mock('googleapis', () => { class MockOAuth2 { setCredentials(): void { // no-op } } return { google: { auth: { OAuth2: MockOAuth2, }, youtube: () => ({ videos: { insert: mockVideosInsert, }, }), }, } }) const mockExistsSync = vi.fn() const mockCreateReadStream = vi.fn() vi.mock('fs', () => ({ default: { existsSync: (...args: unknown[]) => mockExistsSync(...args), createReadStream: (...args: unknown[]) => mockCreateReadStream(...args), }, existsSync: (...args: unknown[]) => mockExistsSync(...args), createReadStream: (...args: unknown[]) => mockCreateReadStream(...args), })) const mockFindByID = vi.fn() const mockFind = vi.fn() const mockPayload = { findByID: mockFindByID, find: mockFind, } as unknown as import('payload').Payload // --- Helpers --- function setupYouTubePlatform(): void { mockFind.mockResolvedValueOnce({ docs: [{ id: 100, slug: 'youtube' }], totalDocs: 1, }) } function setupSocialAccount(overrides?: Record): void { mockFind.mockResolvedValueOnce({ docs: [ { id: 10, isActive: true, credentials: { accessToken: 'yt-access-token', refreshToken: 'yt-refresh-token', }, ...overrides, }, ], totalDocs: 1, }) } function setupMedia(overrides?: Record): void { mockFindByID.mockResolvedValueOnce({ id: 1, filename: 'test-video.mp4', mimeType: 'video/mp4', ...overrides, }) } const defaultMetadata = { title: 'My Test Video', description: 'A great video about testing', tags: ['test', 'video'], visibility: 'public' as const, categoryId: '22', } // --- Tests --- describe('VideoUploadService', () => { beforeEach(() => { vi.clearAllMocks() process.env.GOOGLE_CLIENT_ID = 'test-client-id' process.env.GOOGLE_CLIENT_SECRET = 'test-client-secret' }) it('should upload a video and return the YouTube video ID', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockResolvedValueOnce({ data: { id: 'yt-abc123' }, }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(true) expect(result.youtubeVideoId).toBe('yt-abc123') expect(result.youtubeUrl).toBe('https://www.youtube.com/watch?v=yt-abc123') expect(result.error).toBeUndefined() expect(mockVideosInsert).toHaveBeenCalledWith( expect.objectContaining({ part: ['snippet', 'status'], requestBody: expect.objectContaining({ snippet: { title: 'My Test Video', description: 'A great video about testing', tags: ['test', 'video'], categoryId: '22', }, status: { privacyStatus: 'public', }, }), media: { body: 'fake-stream' }, }), ) }) it('should set privacyStatus to private and include publishAt for scheduled uploads', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockResolvedValueOnce({ data: { id: 'yt-scheduled-1' }, }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: { ...defaultMetadata, visibility: 'public' }, scheduledPublishAt: '2026-03-01T12:00:00Z', }) expect(result.success).toBe(true) expect(result.youtubeVideoId).toBe('yt-scheduled-1') expect(mockVideosInsert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ status: { privacyStatus: 'private', publishAt: '2026-03-01T12:00:00Z', }, }), }), ) }) it('should return error when no YouTube social account exists', async () => { // Platform found, but no account setupYouTubePlatform() mockFind.mockResolvedValueOnce({ docs: [], totalDocs: 0 }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('No active YouTube social account') expect(mockFindByID).not.toHaveBeenCalled() }) it('should return error when social account has no credentials', async () => { setupYouTubePlatform() setupSocialAccount({ credentials: {} }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('No active YouTube social account') expect(mockFindByID).not.toHaveBeenCalled() }) it('should return error when YouTube platform is not found', async () => { mockFind.mockResolvedValueOnce({ docs: [], totalDocs: 0 }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('No active YouTube social account') }) it('should return error when media document is not found', async () => { setupYouTubePlatform() setupSocialAccount() mockFindByID.mockResolvedValueOnce(null) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 999, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('Media with ID 999 not found') }) it('should return error when media has no filename', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia({ filename: undefined }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('has no filename') }) it('should return error when file does not exist on disk', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(false) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('File not found on disk') expect(result.error).toContain('test-video.mp4') }) it('should return error when YouTube API returns no video ID', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockResolvedValueOnce({ data: { id: null }, }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('YouTube API returned no video ID') }) it('should return error when YouTube API throws', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockRejectedValueOnce(new Error('quotaExceeded')) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: defaultMetadata, }) expect(result.success).toBe(false) expect(result.error).toContain('YouTube upload failed') expect(result.error).toContain('quotaExceeded') }) it('should support unlisted visibility', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockResolvedValueOnce({ data: { id: 'yt-unlisted-1' }, }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const result = await service.uploadVideo({ mediaId: 1, metadata: { ...defaultMetadata, visibility: 'unlisted' }, }) expect(result.success).toBe(true) expect(mockVideosInsert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ status: { privacyStatus: 'unlisted' }, }), }), ) }) it('should omit categoryId from snippet when not provided', async () => { setupYouTubePlatform() setupSocialAccount() setupMedia() mockExistsSync.mockReturnValue(true) mockCreateReadStream.mockReturnValue('fake-stream') mockVideosInsert.mockResolvedValueOnce({ data: { id: 'yt-nocat-1' }, }) const { VideoUploadService } = await import( '@/lib/integrations/youtube/VideoUploadService' ) const service = new VideoUploadService(mockPayload) const { categoryId: _, ...metadataWithoutCategory } = defaultMetadata const result = await service.uploadVideo({ mediaId: 1, metadata: metadataWithoutCategory, }) expect(result.success).toBe(true) expect(mockVideosInsert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ snippet: expect.objectContaining({ title: 'My Test Video', categoryId: undefined, }), }), }), ) }) })