From 097bc5225c21637db8b7e68026a0f4c420f03d89 Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Sat, 14 Feb 2026 13:19:23 +0000 Subject: [PATCH] feat(youtube): add upload and analytics OAuth scopes Add youtube.upload and yt-analytics.readonly scopes to enable video uploading and analytics data retrieval in the YouTube Operations Hub. Co-Authored-By: Claude Opus 4.6 --- src/lib/integrations/youtube/oauth.ts | 2 + tests/unit/youtube/oauth-scopes.unit.spec.ts | 75 ++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/unit/youtube/oauth-scopes.unit.spec.ts diff --git a/src/lib/integrations/youtube/oauth.ts b/src/lib/integrations/youtube/oauth.ts index 935bd30..115b39c 100644 --- a/src/lib/integrations/youtube/oauth.ts +++ b/src/lib/integrations/youtube/oauth.ts @@ -10,6 +10,8 @@ const SCOPES = [ 'https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/youtube.force-ssl', 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtube.upload', + 'https://www.googleapis.com/auth/yt-analytics.readonly', ] /** diff --git a/tests/unit/youtube/oauth-scopes.unit.spec.ts b/tests/unit/youtube/oauth-scopes.unit.spec.ts new file mode 100644 index 0000000..7ffe351 --- /dev/null +++ b/tests/unit/youtube/oauth-scopes.unit.spec.ts @@ -0,0 +1,75 @@ +/** + * YouTube OAuth Scopes Unit Tests + * + * Verifies that all required OAuth scopes are present in the YouTube OAuth configuration, + * including upload and analytics scopes needed for the YouTube Operations Hub. + */ + +import { describe, it, expect, vi } from 'vitest' + +vi.mock('googleapis', () => { + class MockOAuth2 { + generateAuthUrl({ scope }: { scope: string[] }): string { + return `https://accounts.google.com/o/oauth2/v2/auth?scope=${encodeURIComponent(scope.join(' '))}` + } + } + + return { + google: { + auth: { + OAuth2: MockOAuth2, + }, + }, + } +}) + +vi.stubEnv('YOUTUBE_CLIENT_ID', 'test-client-id') +vi.stubEnv('YOUTUBE_CLIENT_SECRET', 'test-client-secret') +vi.stubEnv('YOUTUBE_REDIRECT_URI', 'https://test.example.com/api/youtube/callback') + +describe('YouTube OAuth Scopes', () => { + it('should include youtube.readonly scope', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + expect(url).toContain('youtube.readonly') + }) + + it('should include youtube.force-ssl scope', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + expect(url).toContain('youtube.force-ssl') + }) + + it('should include youtube scope (full access)', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + expect(url).toContain('auth%2Fyoutube') + }) + + it('should include youtube.upload scope', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + expect(url).toContain('youtube.upload') + }) + + it('should include yt-analytics.readonly scope', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + expect(url).toContain('yt-analytics.readonly') + }) + + it('should contain exactly 5 scopes', async () => { + vi.resetModules() + const { getAuthUrl } = await import('@/lib/integrations/youtube/oauth') + const url = getAuthUrl() + const decodedUrl = decodeURIComponent(url) + const scopeParam = decodedUrl.split('scope=')[1] + const scopes = scopeParam.split(' ') + expect(scopes).toHaveLength(5) + }) +})