import { describe, expect, it } from 'vitest' import type { OAuthProvider, OAuthServiceMetadata } from './types' import { getAllOAuthServices, getCanonicalScopesForProvider, getMissingRequiredScopes, getProviderIdFromServiceId, getScopesForService, getServiceByProviderAndId, getServiceConfigByProviderId, getServiceConfigByServiceId, parseProvider, } from './utils' describe('getAllOAuthServices', () => { it.concurrent('should return an array of OAuth services', () => { const services = getAllOAuthServices() expect(services).toBeInstanceOf(Array) expect(services.length).toBeGreaterThan(0) }) it.concurrent('should include all required metadata fields for each service', () => { const services = getAllOAuthServices() services.forEach((service) => { expect(service).toHaveProperty('providerId') expect(service).toHaveProperty('name') expect(service).toHaveProperty('description') expect(service).toHaveProperty('baseProvider') expect(typeof service.providerId).toBe('string') expect(typeof service.name).toBe('string') expect(typeof service.description).toBe('string') expect(typeof service.baseProvider).toBe('string') }) }) it.concurrent('should include Google services', () => { const services = getAllOAuthServices() const gmailService = services.find((s) => s.providerId === 'google-email') expect(gmailService).toBeDefined() expect(gmailService?.name).toBe('Gmail') expect(gmailService?.baseProvider).toBe('google') const driveService = services.find((s) => s.providerId === 'google-drive') expect(driveService).toBeDefined() expect(driveService?.name).toBe('Google Drive') expect(driveService?.baseProvider).toBe('google') }) it.concurrent('should include Microsoft services', () => { const services = getAllOAuthServices() const outlookService = services.find((s) => s.providerId === 'outlook') expect(outlookService).toBeDefined() expect(outlookService?.name).toBe('Outlook') expect(outlookService?.baseProvider).toBe('microsoft') const excelService = services.find((s) => s.providerId === 'microsoft-excel') expect(excelService).toBeDefined() expect(excelService?.name).toBe('Microsoft Excel') expect(excelService?.baseProvider).toBe('microsoft') }) it.concurrent('should include single-service providers', () => { const services = getAllOAuthServices() const slackService = services.find((s) => s.providerId === 'slack') expect(slackService).toBeDefined() expect(slackService?.name).toBe('Slack') expect(slackService?.baseProvider).toBe('slack') }) it.concurrent('should not include duplicate services', () => { const services = getAllOAuthServices() const providerIds = services.map((s) => s.providerId) const uniqueProviderIds = new Set(providerIds) expect(providerIds.length).toBe(uniqueProviderIds.size) }) it.concurrent('should return services that match the OAuthServiceMetadata interface', () => { const services = getAllOAuthServices() services.forEach((service) => { const metadata: OAuthServiceMetadata = service expect(metadata.providerId).toBeDefined() expect(metadata.name).toBeDefined() expect(metadata.description).toBeDefined() expect(metadata.baseProvider).toBeDefined() }) }) }) describe('getServiceByProviderAndId', () => { it.concurrent('should return default service when no serviceId is provided', () => { const service = getServiceByProviderAndId('google') expect(service).toBeDefined() expect(service.providerId).toBe('google-email') expect(service.name).toBe('Gmail') }) it.concurrent('should return specific service when serviceId is provided', () => { const service = getServiceByProviderAndId('google', 'google-drive') expect(service).toBeDefined() expect(service.providerId).toBe('google-drive') expect(service.name).toBe('Google Drive') }) it.concurrent('should return default service when invalid serviceId is provided', () => { const service = getServiceByProviderAndId('google', 'invalid-service') expect(service).toBeDefined() expect(service.providerId).toBe('google-email') expect(service.name).toBe('Gmail') }) it.concurrent('should throw error for invalid provider', () => { expect(() => { getServiceByProviderAndId('invalid-provider' as OAuthProvider) }).toThrow('Provider invalid-provider not found') }) it.concurrent('should work with Microsoft provider', () => { const service = getServiceByProviderAndId('microsoft') expect(service).toBeDefined() expect(service.providerId).toBe('outlook') expect(service.name).toBe('Outlook') }) it.concurrent('should work with Microsoft Excel serviceId', () => { const service = getServiceByProviderAndId('microsoft', 'microsoft-excel') expect(service).toBeDefined() expect(service.providerId).toBe('microsoft-excel') expect(service.name).toBe('Microsoft Excel') }) it.concurrent('should include scopes in returned service config', () => { const service = getServiceByProviderAndId('google', 'gmail') expect(service.scopes).toBeDefined() expect(Array.isArray(service.scopes)).toBe(true) expect(service.scopes.length).toBeGreaterThan(0) expect(service.scopes).toContain('https://www.googleapis.com/auth/gmail.send') }) }) describe('getProviderIdFromServiceId', () => { it.concurrent('should return correct providerId for Gmail', () => { const providerId = getProviderIdFromServiceId('gmail') expect(providerId).toBe('google-email') }) it.concurrent('should return correct providerId for Google Drive', () => { const providerId = getProviderIdFromServiceId('google-drive') expect(providerId).toBe('google-drive') }) it.concurrent('should return correct providerId for Outlook', () => { const providerId = getProviderIdFromServiceId('outlook') expect(providerId).toBe('outlook') }) it.concurrent('should return correct providerId for Microsoft Excel', () => { const providerId = getProviderIdFromServiceId('microsoft-excel') expect(providerId).toBe('microsoft-excel') }) it.concurrent('should return serviceId as fallback for unknown service', () => { const providerId = getProviderIdFromServiceId('unknown-service') expect(providerId).toBe('unknown-service') }) it.concurrent('should handle empty string', () => { const providerId = getProviderIdFromServiceId('') expect(providerId).toBe('') }) it.concurrent('should work for all Google services', () => { const googleServices = [ { serviceId: 'gmail', expectedProviderId: 'google-email' }, { serviceId: 'google-drive', expectedProviderId: 'google-drive' }, { serviceId: 'google-docs', expectedProviderId: 'google-docs' }, { serviceId: 'google-sheets', expectedProviderId: 'google-sheets' }, { serviceId: 'google-forms', expectedProviderId: 'google-forms' }, { serviceId: 'google-calendar', expectedProviderId: 'google-calendar' }, { serviceId: 'google-vault', expectedProviderId: 'google-vault' }, { serviceId: 'google-groups', expectedProviderId: 'google-groups' }, { serviceId: 'vertex-ai', expectedProviderId: 'vertex-ai' }, ] googleServices.forEach(({ serviceId, expectedProviderId }) => { expect(getProviderIdFromServiceId(serviceId)).toBe(expectedProviderId) }) }) }) describe('getServiceConfigByProviderId', () => { it.concurrent('should return service config for valid providerId', () => { const service = getServiceConfigByProviderId('google-email') expect(service).toBeDefined() expect(service?.providerId).toBe('google-email') expect(service?.name).toBe('Gmail') }) it.concurrent('should return service config for service key', () => { const service = getServiceConfigByProviderId('gmail') expect(service).toBeDefined() expect(service?.providerId).toBe('google-email') expect(service?.name).toBe('Gmail') }) it.concurrent('should return null for invalid providerId', () => { const service = getServiceConfigByProviderId('invalid-provider') expect(service).toBeNull() }) it.concurrent('should work for Microsoft services', () => { const outlookService = getServiceConfigByProviderId('outlook') expect(outlookService).toBeDefined() expect(outlookService?.providerId).toBe('outlook') expect(outlookService?.name).toBe('Outlook') const excelService = getServiceConfigByProviderId('microsoft-excel') expect(excelService).toBeDefined() expect(excelService?.providerId).toBe('microsoft-excel') expect(excelService?.name).toBe('Microsoft Excel') }) it.concurrent('should work for Slack', () => { const service = getServiceConfigByProviderId('slack') expect(service).toBeDefined() expect(service?.providerId).toBe('slack') expect(service?.name).toBe('Slack') }) it.concurrent('should return service with scopes', () => { const service = getServiceConfigByProviderId('google-drive') expect(service).toBeDefined() expect(service?.scopes).toBeDefined() expect(Array.isArray(service?.scopes)).toBe(true) expect(service?.scopes.length).toBeGreaterThan(0) }) it.concurrent('should handle empty string', () => { const service = getServiceConfigByProviderId('') expect(service).toBeNull() }) }) describe('getServiceConfigByServiceId', () => { it.concurrent('should return service config for a service key', () => { const service = getServiceConfigByServiceId('gmail') expect(service).toBeDefined() expect(service?.providerId).toBe('google-email') expect(service?.name).toBe('Gmail') }) it.concurrent('should resolve the shared Jira service used by Jira Service Management', () => { const service = getServiceConfigByServiceId('jira') expect(service).toBeDefined() expect(service?.providerId).toBe('jira') expect(service?.name).toBe('Jira') }) it.concurrent('should not match on providerId values that are not service keys', () => { const service = getServiceConfigByServiceId('google-email') expect(service).toBeNull() }) it.concurrent('should return null for unknown service id', () => { const service = getServiceConfigByServiceId('invalid-service') expect(service).toBeNull() }) }) describe('getCanonicalScopesForProvider', () => { it.concurrent('should return scopes for valid providerId', () => { const scopes = getCanonicalScopesForProvider('google-email') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBeGreaterThan(0) expect(scopes).toContain('https://www.googleapis.com/auth/gmail.send') expect(scopes).toContain('https://www.googleapis.com/auth/gmail.modify') }) it.concurrent('should return new array instance (not reference)', () => { const scopes1 = getCanonicalScopesForProvider('google-email') const scopes2 = getCanonicalScopesForProvider('google-email') expect(scopes1).not.toBe(scopes2) expect(scopes1).toEqual(scopes2) }) it.concurrent('should return empty array for invalid providerId', () => { const scopes = getCanonicalScopesForProvider('invalid-provider') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBe(0) }) it.concurrent('should work for service key', () => { const scopes = getCanonicalScopesForProvider('gmail') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBeGreaterThan(0) }) it.concurrent('should return scopes for Microsoft services', () => { const outlookScopes = getCanonicalScopesForProvider('outlook') expect(outlookScopes.length).toBeGreaterThan(0) expect(outlookScopes).toContain('Mail.ReadWrite') const excelScopes = getCanonicalScopesForProvider('microsoft-excel') expect(excelScopes.length).toBeGreaterThan(0) expect(excelScopes).toContain('Files.Read') }) it.concurrent('should handle providers with empty scopes array', () => { const scopes = getCanonicalScopesForProvider('notion') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBe(0) }) it.concurrent('should return empty array for empty string', () => { const scopes = getCanonicalScopesForProvider('') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBe(0) }) }) describe('parseProvider', () => { it.concurrent('should parse simple provider without hyphen', () => { const config = parseProvider('slack' as OAuthProvider) expect(config.baseProvider).toBe('slack') expect(config.featureType).toBe('slack') }) it.concurrent('should parse compound provider', () => { const config = parseProvider('google-email' as OAuthProvider) expect(config.baseProvider).toBe('google') expect(config.featureType).toBe('gmail') }) it.concurrent('should use mapping for known providerId', () => { const config = parseProvider('google-drive' as OAuthProvider) expect(config.baseProvider).toBe('google') expect(config.featureType).toBe('google-drive') }) it.concurrent('should parse Microsoft services', () => { const outlookConfig = parseProvider('outlook' as OAuthProvider) expect(outlookConfig.baseProvider).toBe('microsoft') expect(outlookConfig.featureType).toBe('outlook') const excelConfig = parseProvider('microsoft-excel' as OAuthProvider) expect(excelConfig.baseProvider).toBe('microsoft') expect(excelConfig.featureType).toBe('microsoft-excel') const teamsConfig = parseProvider('microsoft-teams' as OAuthProvider) expect(teamsConfig.baseProvider).toBe('microsoft') expect(teamsConfig.featureType).toBe('microsoft-teams') }) it.concurrent('should parse Slack provider', () => { const config = parseProvider('slack' as OAuthProvider) expect(config.baseProvider).toBe('slack') expect(config.featureType).toBe('slack') }) it.concurrent('should parse X provider', () => { const config = parseProvider('x' as OAuthProvider) expect(config.baseProvider).toBe('x') expect(config.featureType).toBe('x') }) it.concurrent('should parse all Google services correctly', () => { const googleServices: Array<{ provider: OAuthProvider; expectedFeature: string }> = [ { provider: 'google-email', expectedFeature: 'gmail' }, { provider: 'google-drive', expectedFeature: 'google-drive' }, { provider: 'google-docs', expectedFeature: 'google-docs' }, { provider: 'google-sheets', expectedFeature: 'google-sheets' }, { provider: 'google-forms', expectedFeature: 'google-forms' }, { provider: 'google-calendar', expectedFeature: 'google-calendar' }, { provider: 'google-vault', expectedFeature: 'google-vault' }, { provider: 'google-groups', expectedFeature: 'google-groups' }, { provider: 'vertex-ai', expectedFeature: 'vertex-ai' }, ] googleServices.forEach(({ provider, expectedFeature }) => { const config = parseProvider(provider) expect(config.baseProvider).toBe('google') expect(config.featureType).toBe(expectedFeature) }) }) it.concurrent('should parse Confluence provider', () => { const config = parseProvider('confluence' as OAuthProvider) expect(config.baseProvider).toBe('confluence') expect(config.featureType).toBe('confluence') }) it.concurrent('should parse Jira provider', () => { const config = parseProvider('jira' as OAuthProvider) expect(config.baseProvider).toBe('jira') expect(config.featureType).toBe('jira') }) it.concurrent('should parse Airtable provider', () => { const config = parseProvider('airtable' as OAuthProvider) expect(config.baseProvider).toBe('airtable') expect(config.featureType).toBe('airtable') }) it.concurrent('should parse Notion provider', () => { const config = parseProvider('notion' as OAuthProvider) expect(config.baseProvider).toBe('notion') expect(config.featureType).toBe('notion') }) it.concurrent('should parse Linear provider', () => { const config = parseProvider('linear' as OAuthProvider) expect(config.baseProvider).toBe('linear') expect(config.featureType).toBe('linear') }) it.concurrent('should parse Dropbox provider', () => { const config = parseProvider('dropbox' as OAuthProvider) expect(config.baseProvider).toBe('dropbox') expect(config.featureType).toBe('dropbox') }) it.concurrent('should parse Shopify provider', () => { const config = parseProvider('shopify' as OAuthProvider) expect(config.baseProvider).toBe('shopify') expect(config.featureType).toBe('shopify') }) it.concurrent('should parse Reddit provider', () => { const config = parseProvider('reddit' as OAuthProvider) expect(config.baseProvider).toBe('reddit') expect(config.featureType).toBe('reddit') }) it.concurrent('should parse Wealthbox provider', () => { const config = parseProvider('wealthbox' as OAuthProvider) expect(config.baseProvider).toBe('wealthbox') expect(config.featureType).toBe('wealthbox') }) it.concurrent('should parse Webflow provider', () => { const config = parseProvider('webflow' as OAuthProvider) expect(config.baseProvider).toBe('webflow') expect(config.featureType).toBe('webflow') }) it.concurrent('should parse Trello provider', () => { const config = parseProvider('trello' as OAuthProvider) expect(config.baseProvider).toBe('trello') expect(config.featureType).toBe('trello') }) it.concurrent('should parse Asana provider', () => { const config = parseProvider('asana' as OAuthProvider) expect(config.baseProvider).toBe('asana') expect(config.featureType).toBe('asana') }) it.concurrent('should parse Pipedrive provider', () => { const config = parseProvider('pipedrive' as OAuthProvider) expect(config.baseProvider).toBe('pipedrive') expect(config.featureType).toBe('pipedrive') }) it.concurrent('should parse HubSpot provider', () => { const config = parseProvider('hubspot' as OAuthProvider) expect(config.baseProvider).toBe('hubspot') expect(config.featureType).toBe('hubspot') }) it.concurrent('should parse LinkedIn provider', () => { const config = parseProvider('linkedin' as OAuthProvider) expect(config.baseProvider).toBe('linkedin') expect(config.featureType).toBe('linkedin') }) it.concurrent('should parse Salesforce provider', () => { const config = parseProvider('salesforce' as OAuthProvider) expect(config.baseProvider).toBe('salesforce') expect(config.featureType).toBe('salesforce') }) it.concurrent('should parse Zoom provider', () => { const config = parseProvider('zoom' as OAuthProvider) expect(config.baseProvider).toBe('zoom') expect(config.featureType).toBe('zoom') }) it.concurrent('should parse WordPress provider', () => { const config = parseProvider('wordpress' as OAuthProvider) expect(config.baseProvider).toBe('wordpress') expect(config.featureType).toBe('wordpress') }) it.concurrent('should parse Spotify provider', () => { const config = parseProvider('spotify' as OAuthProvider) expect(config.baseProvider).toBe('spotify') expect(config.featureType).toBe('spotify') }) it.concurrent('should fallback to default for unknown compound provider', () => { const config = parseProvider('unknown-provider' as OAuthProvider) expect(config.baseProvider).toBe('unknown') expect(config.featureType).toBe('provider') }) it.concurrent('should use default featureType for simple unknown provider', () => { const config = parseProvider('unknown' as OAuthProvider) expect(config.baseProvider).toBe('unknown') expect(config.featureType).toBe('default') }) it.concurrent('should parse OneDrive provider correctly', () => { const config = parseProvider('onedrive' as OAuthProvider) expect(config.baseProvider).toBe('microsoft') expect(config.featureType).toBe('onedrive') }) it.concurrent('should parse SharePoint provider correctly', () => { const config = parseProvider('sharepoint' as OAuthProvider) expect(config.baseProvider).toBe('microsoft') expect(config.featureType).toBe('sharepoint') }) }) describe('getScopesForService', () => { it.concurrent('should return scopes for a valid serviceId', () => { const scopes = getScopesForService('gmail') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBeGreaterThan(0) expect(scopes).toContain('https://www.googleapis.com/auth/gmail.send') }) it.concurrent('should return empty array for unknown serviceId', () => { const scopes = getScopesForService('nonexistent-service') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBe(0) }) it.concurrent('should return new array instance (not reference)', () => { const scopes1 = getScopesForService('gmail') const scopes2 = getScopesForService('gmail') expect(scopes1).not.toBe(scopes2) expect(scopes1).toEqual(scopes2) }) it.concurrent('should work for Microsoft services', () => { const scopes = getScopesForService('outlook') expect(scopes.length).toBeGreaterThan(0) expect(scopes).toContain('Mail.ReadWrite') }) it.concurrent('should return empty array for empty string', () => { const scopes = getScopesForService('') expect(Array.isArray(scopes)).toBe(true) expect(scopes.length).toBe(0) }) }) describe('getMissingRequiredScopes', () => { it.concurrent('should return empty array when all scopes are granted', () => { const credential = { scopes: ['read', 'write'] } const missing = getMissingRequiredScopes(credential, ['read', 'write']) expect(missing).toEqual([]) }) it.concurrent('should return missing scopes', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential, ['read', 'write']) expect(missing).toEqual(['write']) }) it.concurrent('should return all required scopes when credential is undefined', () => { const missing = getMissingRequiredScopes(undefined, ['read', 'write']) expect(missing).toEqual(['read', 'write']) }) it.concurrent('should return all required scopes when credential has undefined scopes', () => { const missing = getMissingRequiredScopes({ scopes: undefined }, ['read', 'write']) expect(missing).toEqual(['read', 'write']) }) it.concurrent('should ignore offline_access in required scopes', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential, ['read', 'offline_access']) expect(missing).toEqual([]) }) it.concurrent('should ignore refresh_token in required scopes', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential, ['read', 'refresh_token']) expect(missing).toEqual([]) }) it.concurrent('should ignore offline.access in required scopes', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential, ['read', 'offline.access']) expect(missing).toEqual([]) }) it.concurrent('should filter ignored scopes even when credential is undefined', () => { const missing = getMissingRequiredScopes(undefined, ['read', 'offline_access', 'refresh_token']) expect(missing).toEqual(['read']) }) it.concurrent('should return empty array when requiredScopes is empty', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential, []) expect(missing).toEqual([]) }) it.concurrent('should return empty array when requiredScopes defaults to empty', () => { const credential = { scopes: ['read'] } const missing = getMissingRequiredScopes(credential) expect(missing).toEqual([]) }) })