/** * Tests for OAuth token API routes * * @vitest-environment node */ import { authOAuthUtilsMock, authOAuthUtilsMockFns, createMockRequest, hybridAuthMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockAuthorizeCredentialUse } = vi.hoisted(() => ({ mockAuthorizeCredentialUse: vi.fn(), })) vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock) vi.mock('@/lib/auth/credential-access', () => ({ authorizeCredentialUse: mockAuthorizeCredentialUse, })) import { GET, POST } from '@/app/api/auth/oauth/token/route' describe('OAuth Token API Routes', () => { beforeEach(() => { vi.clearAllMocks() authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValue(null) }) /** * POST route tests */ describe('POST handler', () => { it('should return access token successfully', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'owner-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: 'test-token', refreshToken: 'refresh-token', accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), providerId: 'google', }) authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({ accessToken: 'fresh-token', refreshed: false, }) const req = createMockRequest('POST', { credentialId: 'credential-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(200) expect(data).toHaveProperty('accessToken', 'fresh-token') expect(mockAuthorizeCredentialUse).toHaveBeenCalled() expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled() expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).toHaveBeenCalled() }) it('should handle workflowId for server-side authentication', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'internal_jwt', requesterUserId: 'workflow-owner-id', credentialOwnerUserId: 'workflow-owner-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: 'test-token', refreshToken: 'refresh-token', accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), providerId: 'google', }) authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({ accessToken: 'fresh-token', refreshed: false, }) const req = createMockRequest('POST', { credentialId: 'credential-id', workflowId: 'workflow-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(200) expect(data).toHaveProperty('accessToken', 'fresh-token') expect(mockAuthorizeCredentialUse).toHaveBeenCalled() expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled() }) it('should handle missing credentialId', async () => { const req = createMockRequest('POST', {}) const response = await POST(req) const data = await response.json() expect(response.status).toBe(400) expect(data).toHaveProperty( 'error', 'Either credentialId or (credentialAccountUserId + providerId) is required' ) }) it('should handle authentication failure', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Authentication required', }) const req = createMockRequest('POST', { credentialId: 'credential-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(403) expect(data).toHaveProperty('error') }) it('should handle workflow not found', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Workflow not found' }) const req = createMockRequest('POST', { credentialId: 'credential-id', workflowId: 'nonexistent-workflow-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(403) }) it('should handle credential not found', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'owner-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce(undefined) const req = createMockRequest('POST', { credentialId: 'nonexistent-credential-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(404) expect(data).toHaveProperty('error') }) it('should handle token refresh failure', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'owner-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: 'test-token', refreshToken: 'refresh-token', accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired providerId: 'google', }) authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockRejectedValueOnce( new Error('Refresh failure') ) const req = createMockRequest('POST', { credentialId: 'credential-id', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(401) expect(data).toHaveProperty('error', 'Failed to refresh access token') }) describe('credentialAccountUserId + providerId path', () => { it('should reject unauthenticated requests', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: false, error: 'Authentication required', }) const req = createMockRequest('POST', { credentialAccountUserId: 'target-user-id', providerId: 'google', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(401) expect(data).toHaveProperty('error', 'User not authenticated') expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled() }) it('should reject internal JWT authentication', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'internal_jwt', userId: 'test-user-id', }) const req = createMockRequest('POST', { credentialAccountUserId: 'test-user-id', providerId: 'google', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(401) expect(data).toHaveProperty('error', 'User not authenticated') expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled() }) it('should reject requests for other users credentials', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'attacker-user-id', }) const req = createMockRequest('POST', { credentialAccountUserId: 'victim-user-id', providerId: 'google', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(403) expect(data).toHaveProperty('error', 'Unauthorized') expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled() }) it('should allow session-authenticated users to access their own credentials', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetOAuthToken.mockResolvedValueOnce('valid-access-token') const req = createMockRequest('POST', { credentialAccountUserId: 'test-user-id', providerId: 'google', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(200) expect(data).toHaveProperty('accessToken', 'valid-access-token') expect(authOAuthUtilsMockFns.mockGetOAuthToken).toHaveBeenCalledWith( 'test-user-id', 'google' ) }) it('should return 404 when credential not found for user', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetOAuthToken.mockResolvedValueOnce(null) const req = createMockRequest('POST', { credentialAccountUserId: 'test-user-id', providerId: 'nonexistent-provider', }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(404) expect(data.error).toContain('No credential found') }) }) }) /** * GET route tests */ describe('GET handler', () => { it('should return access token successfully', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: 'test-token', refreshToken: 'refresh-token', accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), providerId: 'google', }) authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({ accessToken: 'fresh-token', refreshed: false, }) const req = new NextRequest( 'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id' ) const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(200) expect(data).toHaveProperty('accessToken', 'fresh-token') expect(mockAuthorizeCredentialUse).toHaveBeenCalled() expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled() expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).toHaveBeenCalled() }) it('should handle missing credentialId', async () => { const req = new NextRequest('http://localhost:3000/api/auth/oauth/token') const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(400) expect(data).toHaveProperty('error', 'Credential ID is required') }) it('should handle authentication failure', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Authentication required', }) const req = new NextRequest( 'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id' ) const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(403) expect(data).toHaveProperty('error') }) it('should handle credential not found', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce(undefined) const req = new NextRequest( 'http://localhost:3000/api/auth/oauth/token?credentialId=nonexistent-credential-id' ) const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(404) expect(data).toHaveProperty('error') }) it('should handle missing access token', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: null, refreshToken: 'refresh-token', providerId: 'google', }) const req = new NextRequest( 'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id' ) const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(400) expect(data).toHaveProperty('error') }) it('should handle token refresh failure', async () => { mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: true, authType: 'session', requesterUserId: 'test-user-id', credentialOwnerUserId: 'test-user-id', }) authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ id: 'credential-id', accessToken: 'test-token', refreshToken: 'refresh-token', accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired providerId: 'google', }) authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockRejectedValueOnce( new Error('Refresh failure') ) const req = new NextRequest( 'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id' ) const response = await GET(req as any) const data = await response.json() expect(response.status).toBe(401) expect(data).toHaveProperty('error') }) }) })