UNPKG

@clduab11/gemini-flow

Version:

Revolutionary AI agent swarm coordination platform with Google Services integration, multimedia processing, and production-ready monitoring. Features 8 Google AI services, quantum computing capabilities, and enterprise-grade security.

645 lines (534 loc) 20.2 kB
/** * OAuth2 Provider Unit Tests * * Comprehensive test suite for OAuth2 token refresh mechanism, authentication flows, * and error handling scenarios */ import { jest } from '@jest/globals'; import { OAuth2Provider } from '../../../src/core/auth/oauth2-provider'; import { OAuth2Config, AuthCredentials } from '../../../src/types/auth'; // Mock fetch globally const mockFetch = jest.fn(); global.fetch = mockFetch as any; describe('OAuth2Provider', () => { let provider: OAuth2Provider; let mockConfig: OAuth2Config; beforeEach(() => { // Reset mocks jest.clearAllMocks(); mockFetch.mockClear(); // Default OAuth2 configuration for testing mockConfig = { clientId: 'test-client-id', clientSecret: 'test-client-secret', redirectUri: 'http://localhost:3000/callback', authorizationEndpoint: 'https://auth.example.com/oauth/authorize', tokenEndpoint: 'https://auth.example.com/oauth/token', revokeEndpoint: 'https://auth.example.com/oauth/revoke', userinfoEndpoint: 'https://auth.example.com/oauth/userinfo', scopes: ['read', 'write'], pkceEnabled: true }; provider = new OAuth2Provider(mockConfig); }); afterEach(() => { jest.restoreAllMocks(); }); describe('Configuration Validation', () => { it('should validate required configuration fields', () => { const invalidConfigs = [ { ...mockConfig, clientId: '' }, { ...mockConfig, clientSecret: '' }, { ...mockConfig, redirectUri: '' }, { ...mockConfig, authorizationEndpoint: '' }, { ...mockConfig, tokenEndpoint: '' }, { ...mockConfig, scopes: [] } ]; invalidConfigs.forEach(config => { expect(() => new OAuth2Provider(config)).toThrow(); }); }); it('should validate URL formats', () => { const invalidUrlConfig = { ...mockConfig, authorizationEndpoint: 'invalid-url' }; expect(() => new OAuth2Provider(invalidUrlConfig)).toThrow(/invalid URLs/i); }); it('should accept valid configuration', () => { expect(() => new OAuth2Provider(mockConfig)).not.toThrow(); }); }); describe('Authentication Flow', () => { it('should start authentication flow successfully', async () => { const result = await provider.authenticate(); expect(result.success).toBe(true); expect(result.redirectUrl).toContain(mockConfig.authorizationEndpoint); expect(result.redirectUrl).toContain('client_id=test-client-id'); expect(result.redirectUrl).toContain('response_type=code'); expect(result.redirectUrl).toContain('scope=read+write'); expect(result.context).toBeDefined(); expect(result.context?.sessionId).toBeDefined(); }); it('should include PKCE parameters when enabled', async () => { const result = await provider.authenticate(); expect(result.redirectUrl).toContain('code_challenge='); expect(result.redirectUrl).toContain('code_challenge_method=S256'); expect(result.context?.credentials?.metadata?.pkceVerifier).toBeDefined(); }); it('should include state parameter for CSRF protection', async () => { const result = await provider.authenticate(); expect(result.redirectUrl).toContain('state='); expect(result.context?.credentials?.metadata?.state).toBeDefined(); }); }); describe('Token Exchange', () => { beforeEach(() => { // Mock successful token response mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'test-access-token', refresh_token: 'test-refresh-token', expires_in: 3600, token_type: 'Bearer', scope: 'read write' }) }); }); it('should exchange authorization code for tokens', async () => { const result = await provider.exchangeCodeForTokens( 'test-auth-code', 'test-state', 'test-code-verifier' ); expect(result.success).toBe(true); expect(result.credentials).toBeDefined(); expect(result.credentials?.accessToken).toBe('test-access-token'); expect(result.credentials?.refreshToken).toBe('test-refresh-token'); expect(result.credentials?.expiresAt).toBeGreaterThan(Date.now()); expect(result.credentials?.scope).toEqual(['read', 'write']); // Verify API call expect(mockFetch).toHaveBeenCalledWith( mockConfig.tokenEndpoint, expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/x-www-form-urlencoded' }) }) ); }); it('should validate state parameter', async () => { // First start auth flow to set state await provider.authenticate(); const result = await provider.exchangeCodeForTokens( 'test-auth-code', 'invalid-state' ); expect(result.success).toBe(false); expect(result.error?.message).toContain('Invalid state parameter'); }); it('should handle token exchange errors', async () => { mockFetch.mockResolvedValue({ ok: false, json: jest.fn().mockResolvedValue({ error: 'invalid_grant', error_description: 'The authorization code is invalid' }) }); const result = await provider.exchangeCodeForTokens('invalid-code'); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.error?.code).toBe('TOKEN_EXCHANGE_FAILED'); }); }); describe('Token Refresh', () => { let mockCredentials: AuthCredentials; beforeEach(() => { mockCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'current-access-token', refreshToken: 'current-refresh-token', expiresAt: Date.now() + 300000, // 5 minutes from now scope: ['read', 'write'], issuedAt: Date.now() - 3600000, // 1 hour ago metadata: {} }; }); it('should refresh tokens successfully', async () => { // Mock successful refresh response mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', expires_in: 3600, token_type: 'Bearer', scope: 'read write' }) }); const result = await provider.refresh(mockCredentials); expect(result.success).toBe(true); expect(result.credentials).toBeDefined(); expect(result.credentials?.accessToken).toBe('new-access-token'); expect(result.credentials?.refreshToken).toBe('new-refresh-token'); expect(result.credentials?.metadata?.refreshedAt).toBeDefined(); // Verify refresh request expect(mockFetch).toHaveBeenCalledWith( mockConfig.tokenEndpoint, expect.objectContaining({ method: 'POST', body: expect.stringContaining('grant_type=refresh_token') }) ); }); it('should handle missing refresh token', async () => { const credentialsWithoutRefresh = { ...mockCredentials, refreshToken: undefined }; const result = await provider.refresh(credentialsWithoutRefresh); expect(result.success).toBe(false); expect(result.requiresReauth).toBe(true); expect(result.error?.code).toBe('NO_REFRESH_TOKEN'); }); it('should handle refresh token errors', async () => { mockFetch.mockResolvedValue({ ok: false, json: jest.fn().mockResolvedValue({ error: 'invalid_grant', error_description: 'Refresh token is invalid' }) }); const result = await provider.refresh(mockCredentials); expect(result.success).toBe(false); expect(result.requiresReauth).toBe(true); expect(result.error?.code).toBe('TOKEN_REFRESH_FAILED'); }); it('should preserve existing refresh token if not returned', async () => { mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'new-access-token', expires_in: 3600, token_type: 'Bearer' // No refresh_token in response }) }); const result = await provider.refresh(mockCredentials); expect(result.success).toBe(true); expect(result.credentials?.refreshToken).toBe('current-refresh-token'); }); it('should handle network errors during refresh', async () => { mockFetch.mockRejectedValue(new Error('Network error')); const result = await provider.refresh(mockCredentials); expect(result.success).toBe(false); expect(result.error?.code).toBe('TOKEN_REFRESH_FAILED'); expect(result.error?.originalError?.message).toBe('Network error'); }); }); describe('Token Validation', () => { let validCredentials: AuthCredentials; let expiredCredentials: AuthCredentials; beforeEach(() => { validCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'valid-token', expiresAt: Date.now() + 3600000, // 1 hour from now scope: ['read', 'write'], issuedAt: Date.now(), metadata: {} }; expiredCredentials = { ...validCredentials, expiresAt: Date.now() - 1000 // 1 second ago }; }); it('should validate non-expired tokens', async () => { const result = await provider.validate(validCredentials); expect(result.valid).toBe(true); expect(result.expiresIn).toBeGreaterThan(3500); // ~1 hour expect(result.scopes).toEqual(['read', 'write']); }); it('should detect expired tokens', async () => { const result = await provider.validate(expiredCredentials); expect(result.valid).toBe(false); expect(result.expired).toBe(true); expect(result.error).toContain('expired'); }); it('should handle missing access token', async () => { const invalidCredentials = { ...validCredentials, accessToken: '' }; const result = await provider.validate(invalidCredentials); expect(result.valid).toBe(false); expect(result.error).toContain('No access token'); }); it('should validate with userinfo endpoint when available', async () => { mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ sub: 'user123', name: 'Test User' }) }); const result = await provider.validate(validCredentials); expect(result.valid).toBe(true); expect(mockFetch).toHaveBeenCalledWith( mockConfig.userinfoEndpoint, expect.objectContaining({ headers: expect.objectContaining({ 'Authorization': 'Bearer valid-token' }) }) ); }); it('should handle userinfo endpoint errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 401, statusText: 'Unauthorized' }); const result = await provider.validate(validCredentials); expect(result.valid).toBe(false); expect(result.error).toContain('userinfo endpoint'); }); }); describe('Token Revocation', () => { let mockCredentials: AuthCredentials; beforeEach(() => { mockCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'access-token-to-revoke', refreshToken: 'refresh-token-to-revoke', expiresAt: Date.now() + 3600000, scope: ['read', 'write'], issuedAt: Date.now(), metadata: {} }; }); it('should revoke tokens successfully', async () => { mockFetch.mockResolvedValue({ ok: true }); await expect(provider.revoke(mockCredentials)).resolves.not.toThrow(); // Should call revoke endpoint for both tokens expect(mockFetch).toHaveBeenCalledTimes(2); expect(mockFetch).toHaveBeenCalledWith( mockConfig.revokeEndpoint, expect.objectContaining({ method: 'POST', body: expect.stringContaining('token=access-token-to-revoke') }) ); expect(mockFetch).toHaveBeenCalledWith( mockConfig.revokeEndpoint, expect.objectContaining({ method: 'POST', body: expect.stringContaining('token=refresh-token-to-revoke') }) ); }); it('should handle missing revoke endpoint', async () => { const configWithoutRevoke = { ...mockConfig, revokeEndpoint: undefined }; const providerWithoutRevoke = new OAuth2Provider(configWithoutRevoke); await expect(providerWithoutRevoke.revoke(mockCredentials)).resolves.not.toThrow(); expect(mockFetch).not.toHaveBeenCalled(); }); it('should continue revoking even if one token fails', async () => { mockFetch .mockResolvedValueOnce({ ok: false, status: 400 }) // First token fails .mockResolvedValueOnce({ ok: true }); // Second token succeeds await expect(provider.revoke(mockCredentials)).resolves.not.toThrow(); expect(mockFetch).toHaveBeenCalledTimes(2); }); it('should handle revocation errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(provider.revoke(mockCredentials)).rejects.toThrow(); }); }); describe('PKCE Support', () => { it('should generate valid PKCE parameters', async () => { const result = await provider.authenticate(); expect(result.redirectUrl).toContain('code_challenge='); expect(result.redirectUrl).toContain('code_challenge_method=S256'); // Extract code challenge from URL const urlParams = new URLSearchParams(result.redirectUrl?.split('?')[1]); const codeChallenge = urlParams.get('code_challenge'); const codeChallengeMethod = urlParams.get('code_challenge_method'); expect(codeChallenge).toBeTruthy(); expect(codeChallenge).toMatch(/^[A-Za-z0-9_-]+$/); // Base64URL format expect(codeChallengeMethod).toBe('S256'); }); it('should work without PKCE when disabled', async () => { const configWithoutPKCE = { ...mockConfig, pkceEnabled: false }; const providerWithoutPKCE = new OAuth2Provider(configWithoutPKCE); const result = await providerWithoutPKCE.authenticate(); expect(result.redirectUrl).not.toContain('code_challenge'); expect(result.redirectUrl).not.toContain('code_challenge_method'); }); }); describe('Error Handling', () => { it('should create standardized auth errors', async () => { mockFetch.mockRejectedValue(new Error('Network failure')); const mockCredentials: AuthCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'test-token', refreshToken: 'test-refresh', expiresAt: Date.now() + 3600000, scope: ['read'], issuedAt: Date.now(), metadata: {} }; const result = await provider.refresh(mockCredentials); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.error?.type).toBe('authentication'); expect(result.error?.code).toBe('TOKEN_REFRESH_FAILED'); expect(result.error?.originalError).toBeDefined(); expect(result.error?.context?.provider).toBe('oauth2'); }); it('should handle malformed token responses', async () => { mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ // Missing required fields expires_in: 3600 }) }); const result = await provider.exchangeCodeForTokens('test-code'); expect(result.success).toBe(true); // Should still succeed with partial data expect(result.credentials?.accessToken).toBeUndefined(); }); }); describe('Event Emission', () => { it('should emit authentication events', async () => { const eventSpy = jest.fn(); provider.on('authenticated', eventSpy); mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'test-token', refresh_token: 'test-refresh', expires_in: 3600, token_type: 'Bearer' }) }); await provider.exchangeCodeForTokens('test-code'); expect(eventSpy).toHaveBeenCalledWith( expect.objectContaining({ credentials: expect.any(Object), context: expect.any(Object) }) ); }); it('should emit token refresh events', async () => { const eventSpy = jest.fn(); provider.on('token_refreshed', eventSpy); mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'new-token', expires_in: 3600, token_type: 'Bearer' }) }); const mockCredentials: AuthCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'old-token', refreshToken: 'refresh-token', expiresAt: Date.now() + 300000, scope: ['read'], issuedAt: Date.now(), metadata: {} }; await provider.refresh(mockCredentials); expect(eventSpy).toHaveBeenCalledWith( expect.objectContaining({ credentials: expect.any(Object) }) ); }); it('should emit revocation events', async () => { const eventSpy = jest.fn(); provider.on('tokens_revoked', eventSpy); mockFetch.mockResolvedValue({ ok: true }); const mockCredentials: AuthCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'token-to-revoke', refreshToken: 'refresh-to-revoke', expiresAt: Date.now() + 3600000, scope: ['read'], issuedAt: Date.now(), metadata: {} }; await provider.revoke(mockCredentials); expect(eventSpy).toHaveBeenCalledWith( expect.objectContaining({ credentials: expect.any(Object) }) ); }); }); describe('Security Features', () => { it('should use secure defaults for authorization URL', async () => { const result = await provider.authenticate(); const url = new URL(result.redirectUrl!); const params = url.searchParams; expect(params.get('access_type')).toBe('offline'); expect(params.get('prompt')).toBe('consent'); expect(params.get('state')).toBeTruthy(); }); it('should include proper User-Agent header', async () => { mockFetch.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ access_token: 'test-token', expires_in: 3600 }) }); await provider.exchangeCodeForTokens('test-code'); expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': 'GeminiFlow/1.1.0 OAuth2Provider' }) }) ); }); it('should detect retryable vs non-retryable errors', async () => { const networkError = new Error('Network timeout'); const authError = new Error('invalid_grant: Invalid authorization code'); // Both should be handled, but retryable flag should differ const mockCredentials: AuthCredentials = { type: 'oauth2', provider: 'oauth2', accessToken: 'test-token', refreshToken: 'test-refresh', expiresAt: Date.now() + 3600000, scope: ['read'], issuedAt: Date.now(), metadata: {} }; mockFetch.mockRejectedValueOnce(networkError); const networkResult = await provider.refresh(mockCredentials); mockFetch.mockRejectedValueOnce(authError); const authResult = await provider.refresh(mockCredentials); expect(networkResult.error?.retryable).toBe(true); expect(authResult.error?.retryable).toBe(true); // TOKEN_REFRESH_FAILED is retryable }); }); });