/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi } from 'vitest';
// Mock dependencies AT THE TOP
const mockOpenBrowserSecurely = vi.hoisted(() => vi.fn());
vi.mock('../utils/secure-browser-launcher.js', () => ({
    openBrowserSecurely: mockOpenBrowserSecurely,
}));
vi.mock('node:crypto');
vi.mock('./oauth-token-storage.js', () => {
    const mockSaveToken = vi.fn();
    const mockGetCredentials = vi.fn();
    const mockIsTokenExpired = vi.fn();
    const mockdeleteCredentials = vi.fn();
    return {
        MCPOAuthTokenStorage: vi.fn(() => ({
            saveToken: mockSaveToken,
            getCredentials: mockGetCredentials,
            isTokenExpired: mockIsTokenExpired,
            deleteCredentials: mockdeleteCredentials,
        })),
    };
});
vi.mock('../utils/events.js', () => ({
    coreEvents: {
        emitFeedback: vi.fn(),
        emitConsoleLog: vi.fn(),
    },
}));
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import { MCPOAuthProvider } from './oauth-provider.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
import { OAuthUtils, } from './oauth-utils.js';
import { coreEvents } from '../utils/events.js';
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Helper function to create mock fetch responses with proper headers
const createMockResponse = (options) => {
    const response = {
        ok: options.ok,
        headers: {
            get: (name) => {
                if (name.toLowerCase() === 'content-type') {
                    return options.contentType || null;
                }
                return null;
            },
        },
    };
    if (options.status !== undefined) {
        response.status = options.status;
    }
    if (options.text !== undefined) {
        response.text =
            typeof options.text === 'string'
                ? () => Promise.resolve(options.text)
                : options.text;
    }
    if (options.json !== undefined) {
        response.json =
            typeof options.json === 'function'
                ? options.json
                : () => Promise.resolve(options.json);
    }
    return response;
};
// Define a reusable mock server with .listen, .close, .on, and .address methods
const mockHttpServer = {
    listen: vi.fn(),
    close: vi.fn(),
    on: vi.fn(),
    address: vi.fn(() => ({ address: 'localhost', family: 'IPv4', port: 7777 })),
};
vi.mock('node:http', () => ({
    createServer: vi.fn(() => mockHttpServer),
}));
describe('MCPOAuthProvider', () => {
    const mockConfig = {
        enabled: true,
        clientId: 'test-client-id',
        clientSecret: 'test-client-secret',
        authorizationUrl: 'https://auth.example.com/authorize',
        tokenUrl: 'https://auth.example.com/token',
        scopes: ['read', 'write'],
        redirectUri: 'http://localhost:7777/oauth/callback',
        audiences: ['https://api.example.com'],
    };
    const mockToken = {
        accessToken: 'access_token_123',
        refreshToken: 'refresh_token_456',
        tokenType: 'Bearer',
        scope: 'read write',
        expiresAt: Date.now() + 3600000,
    };
    const mockTokenResponse = {
        access_token: 'access_token_123',
        token_type: 'Bearer',
        expires_in: 3600,
        refresh_token: 'refresh_token_456',
        scope: 'read write',
    };
    beforeEach(() => {
        vi.clearAllMocks();
        mockOpenBrowserSecurely.mockClear();
        vi.spyOn(console, 'log').mockImplementation(() => { });
        vi.spyOn(console, 'warn').mockImplementation(() => { });
        vi.spyOn(console, 'error').mockImplementation(() => { });
        // Mock crypto functions
        vi.mocked(crypto.randomBytes).mockImplementation((size) => {
            if (size === 32)
                return Buffer.from('code_verifier_mock_32_bytes_long');
            if (size === 16)
                return Buffer.from('state_mock_16_by');
            return Buffer.alloc(size);
        });
        vi.mocked(crypto.createHash).mockReturnValue({
            update: vi.fn().mockReturnThis(),
            digest: vi.fn().mockReturnValue('code_challenge_mock'),
        });
        // Mock randomBytes to return predictable values for state
        vi.mocked(crypto.randomBytes).mockImplementation((size) => {
            if (size === 32) {
                return Buffer.from('mock_code_verifier_32_bytes_long_string');
            }
            else if (size === 16) {
                return Buffer.from('mock_state_16_bytes');
            }
            return Buffer.alloc(size);
        });
        // Mock token storage
        const tokenStorage = new MCPOAuthTokenStorage();
        vi.mocked(tokenStorage.saveToken).mockResolvedValue(undefined);
        vi.mocked(tokenStorage.getCredentials).mockResolvedValue(null);
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    describe('authenticate', () => {
        it('should perform complete OAuth flow with PKCE', async () => {
            // Mock HTTP server callback
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                // Simulate OAuth callback
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', mockConfig);
            expect(result).toEqual({
                accessToken: 'access_token_123',
                refreshToken: 'refresh_token_456',
                tokenType: 'Bearer',
                scope: 'read write',
                expiresAt: expect.any(Number),
            });
            expect(mockOpenBrowserSecurely).toHaveBeenCalledWith(expect.stringContaining('authorize'));
            const tokenStorage = new MCPOAuthTokenStorage();
            expect(tokenStorage.saveToken).toHaveBeenCalledWith('test-server', expect.objectContaining({ accessToken: 'access_token_123' }), 'test-client-id', 'https://auth.example.com/token', undefined);
        });
        it('should handle OAuth discovery when no authorization URL provided', async () => {
            // Use a mutable config object
            const configWithoutAuth = {
                ...mockConfig,
                clientId: 'test-client-id',
                clientSecret: 'test-client-secret',
            };
            delete configWithoutAuth.authorizationUrl;
            delete configWithoutAuth.tokenUrl;
            const mockResourceMetadata = {
                authorization_servers: ['https://discovered.auth.com'],
            };
            const mockAuthServerMetadata = {
                authorization_endpoint: 'https://discovered.auth.com/authorize',
                token_endpoint: 'https://discovered.auth.com/token',
                scopes_supported: ['read', 'write'],
            };
            // Mock HEAD request for WWW-Authenticate check
            mockFetch
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                status: 200,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockResourceMetadata),
                json: mockResourceMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockAuthServerMetadata),
                json: mockAuthServerMetadata,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange with discovered endpoint
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', configWithoutAuth, 'https://api.example.com');
            expect(result).toBeDefined();
            expect(mockFetch).toHaveBeenCalledWith('https://discovered.auth.com/token', expect.objectContaining({
                method: 'POST',
                headers: expect.objectContaining({
                    'Content-Type': 'application/x-www-form-urlencoded',
                }),
            }));
        });
        it('should perform dynamic client registration when no client ID is provided but registration URL is provided', async () => {
            const configWithoutClient = {
                ...mockConfig,
                registrationUrl: 'https://auth.example.com/register',
            };
            delete configWithoutClient.clientId;
            const mockRegistrationResponse = {
                client_id: 'dynamic_client_id',
                client_secret: 'dynamic_client_secret',
                redirect_uris: ['http://localhost:7777/oauth/callback'],
                grant_types: ['authorization_code', 'refresh_token'],
                response_types: ['code'],
                token_endpoint_auth_method: 'none',
            };
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockRegistrationResponse),
                json: mockRegistrationResponse,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', configWithoutClient);
            expect(result).toBeDefined();
            expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/register', expect.objectContaining({
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
            }));
        });
        it('should perform OAuth discovery and dynamic client registration when no client ID or registration URL provided', async () => {
            const configWithoutClient = { ...mockConfig };
            delete configWithoutClient.clientId;
            const mockRegistrationResponse = {
                client_id: 'dynamic_client_id',
                client_secret: 'dynamic_client_secret',
                redirect_uris: ['http://localhost:7777/oauth/callback'],
                grant_types: ['authorization_code', 'refresh_token'],
                response_types: ['code'],
                token_endpoint_auth_method: 'none',
            };
            const mockAuthServerMetadata = {
                issuer: 'https://auth.example.com',
                authorization_endpoint: 'https://auth.example.com/authorize',
                token_endpoint: 'https://auth.example.com/token',
                registration_endpoint: 'https://auth.example.com/register',
            };
            mockFetch
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockAuthServerMetadata),
                json: mockAuthServerMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockRegistrationResponse),
                json: mockRegistrationResponse,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', configWithoutClient);
            expect(result).toBeDefined();
            expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/register', expect.objectContaining({
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
            }));
        });
        it('should perform OAuth discovery once and dynamic client registration when no client ID, authorization URL or registration URL provided', async () => {
            const configWithoutClientAndAuthorizationUrl = {
                ...mockConfig,
            };
            delete configWithoutClientAndAuthorizationUrl.clientId;
            delete configWithoutClientAndAuthorizationUrl.authorizationUrl;
            const mockResourceMetadata = {
                resource: 'https://api.example.com',
                authorization_servers: ['https://auth.example.com'],
            };
            const mockAuthServerMetadata = {
                issuer: 'https://auth.example.com',
                authorization_endpoint: 'https://auth.example.com/authorize',
                token_endpoint: 'https://auth.example.com/token',
                registration_endpoint: 'https://auth.example.com/register',
            };
            const mockRegistrationResponse = {
                client_id: 'dynamic_client_id',
                client_secret: 'dynamic_client_secret',
                redirect_uris: ['http://localhost:7777/oauth/callback'],
                grant_types: ['authorization_code', 'refresh_token'],
                response_types: ['code'],
                token_endpoint_auth_method: 'none',
            };
            mockFetch
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                status: 200,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockResourceMetadata),
                json: mockResourceMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockAuthServerMetadata),
                json: mockAuthServerMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockRegistrationResponse),
                json: mockRegistrationResponse,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', configWithoutClientAndAuthorizationUrl, 'https://api.example.com');
            expect(result).toBeDefined();
            expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/register', expect.objectContaining({
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
            }));
        });
        it('should handle OAuth callback errors', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?error=access_denied&error_description=User%20denied%20access',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', mockConfig)).rejects.toThrow('OAuth error: access_denied');
        });
        it('should handle state mismatch in callback', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=wrong_state',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', mockConfig)).rejects.toThrow('State mismatch - possible CSRF attack');
        });
        it('should handle token exchange failure', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 400,
                contentType: 'application/x-www-form-urlencoded',
                text: 'error=invalid_grant&error_description=Invalid grant',
            }));
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', mockConfig)).rejects.toThrow('Token exchange failed: invalid_grant - Invalid grant');
        });
        it('should handle OAuth discovery failure', async () => {
            const configWithoutAuth = { ...mockConfig };
            delete configWithoutAuth.authorizationUrl;
            delete configWithoutAuth.tokenUrl;
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 404,
            }));
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', configWithoutAuth, 'https://api.example.com')).rejects.toThrow('Failed to discover OAuth configuration from MCP server');
        });
        it('should handle authorization server metadata discovery failure', async () => {
            const configWithoutClient = { ...mockConfig };
            delete configWithoutClient.clientId;
            mockFetch.mockResolvedValue(createMockResponse({
                ok: false,
                status: 404,
            }));
            // Prevent callback server from hanging the test
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
            });
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', configWithoutClient)).rejects.toThrow('Failed to fetch authorization server metadata for client registration');
        });
        it('should handle invalid callback request', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/invalid-path',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 0);
            });
            const authProvider = new MCPOAuthProvider();
            // The test will timeout if the server does not handle the invalid request correctly.
            // We are testing that the server does not hang.
            await Promise.race([
                authProvider.authenticate('test-server', mockConfig),
                new Promise((resolve) => setTimeout(resolve, 1000)),
            ]);
        });
        it('should handle token exchange failure with non-json response', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 500,
                contentType: 'text/html',
                text: 'Internal Server Error',
            }));
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', mockConfig)).rejects.toThrow('Token exchange failed: 500 - Internal Server Error');
        });
        it('should handle token exchange with unexpected content type', async () => {
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'text/plain',
                text: 'access_token=plain_text_token',
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', mockConfig);
            expect(result.accessToken).toBe('plain_text_token');
        });
        it('should handle refresh token failure with non-json response', async () => {
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 500,
                contentType: 'text/html',
                text: 'Internal Server Error',
            }));
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.refreshAccessToken(mockConfig, 'invalid_refresh_token', 'https://auth.example.com/token')).rejects.toThrow('Token refresh failed: 500 - Internal Server Error');
        });
        it('should handle refresh token with unexpected content type', async () => {
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'text/plain',
                text: 'access_token=plain_text_token',
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.refreshAccessToken(mockConfig, 'refresh_token', 'https://auth.example.com/token');
            expect(result.access_token).toBe('plain_text_token');
        });
        it('should continue authentication when browser fails to open', async () => {
            mockOpenBrowserSecurely.mockRejectedValue(new Error('Browser not found'));
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.authenticate('test-server', mockConfig);
            expect(result).toBeDefined();
        });
        it('should return null when token is expired and no refresh token is available', async () => {
            const expiredCredentials = {
                serverName: 'test-server',
                token: {
                    ...mockToken,
                    refreshToken: undefined,
                    expiresAt: Date.now() - 3600000,
                },
                clientId: 'test-client-id',
                tokenUrl: 'https://auth.example.com/token',
                updatedAt: Date.now(),
            };
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(expiredCredentials);
            vi.mocked(tokenStorage.isTokenExpired).mockReturnValue(true);
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBeNull();
        });
        it('should handle callback timeout', async () => {
            vi.mocked(http.createServer).mockImplementation(() => mockHttpServer);
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                // Don't trigger callback - simulate timeout
            });
            // Mock setTimeout to trigger timeout immediately
            const originalSetTimeout = global.setTimeout;
            global.setTimeout = vi.fn((callback, delay) => {
                if (delay === 5 * 60 * 1000) {
                    // 5 minute timeout
                    callback();
                }
                return originalSetTimeout(callback, 0);
            });
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.authenticate('test-server', mockConfig)).rejects.toThrow('OAuth callback timeout');
            global.setTimeout = originalSetTimeout;
        });
    });
    describe('refreshAccessToken', () => {
        it('should refresh token successfully', async () => {
            const refreshResponse = {
                access_token: 'new_access_token',
                token_type: 'Bearer',
                expires_in: 3600,
                refresh_token: 'new_refresh_token',
            };
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(refreshResponse),
                json: refreshResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.refreshAccessToken(mockConfig, 'old_refresh_token', 'https://auth.example.com/token');
            expect(result).toEqual(refreshResponse);
            expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/token', expect.objectContaining({
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    Accept: 'application/json, application/x-www-form-urlencoded',
                },
                body: expect.stringContaining('grant_type=refresh_token'),
            }));
        });
        it('should include client secret in refresh request when available', async () => {
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            await authProvider.refreshAccessToken(mockConfig, 'refresh_token', 'https://auth.example.com/token');
            const fetchCall = mockFetch.mock.calls[0];
            expect(fetchCall[1].body).toContain('client_secret=test-client-secret');
        });
        it('should handle refresh token failure', async () => {
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 400,
                contentType: 'application/x-www-form-urlencoded',
                text: 'error=invalid_request&error_description=Invalid refresh token',
            }));
            const authProvider = new MCPOAuthProvider();
            await expect(authProvider.refreshAccessToken(mockConfig, 'invalid_refresh_token', 'https://auth.example.com/token')).rejects.toThrow('Token refresh failed: invalid_request - Invalid refresh token');
        });
    });
    describe('getValidToken', () => {
        it('should return valid token when not expired', async () => {
            const validCredentials = {
                serverName: 'test-server',
                token: mockToken,
                clientId: 'test-client-id',
                tokenUrl: 'https://auth.example.com/token',
                updatedAt: Date.now(),
            };
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(validCredentials);
            vi.mocked(tokenStorage.isTokenExpired).mockReturnValue(false);
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBe('access_token_123');
        });
        it('should refresh expired token and return new token', async () => {
            const expiredCredentials = {
                serverName: 'test-server',
                token: { ...mockToken, expiresAt: Date.now() - 3600000 },
                clientId: 'test-client-id',
                tokenUrl: 'https://auth.example.com/token',
                updatedAt: Date.now(),
            };
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(expiredCredentials);
            vi.mocked(tokenStorage.isTokenExpired).mockReturnValue(true);
            const refreshResponse = {
                access_token: 'new_access_token',
                token_type: 'Bearer',
                expires_in: 3600,
                refresh_token: 'new_refresh_token',
            };
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(refreshResponse),
                json: refreshResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBe('new_access_token');
            expect(tokenStorage.saveToken).toHaveBeenCalledWith('test-server', expect.objectContaining({ accessToken: 'new_access_token' }), 'test-client-id', 'https://auth.example.com/token', undefined);
        });
        it('should return null when no credentials exist', async () => {
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(null);
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBeNull();
        });
        it('should handle refresh failure and remove invalid token', async () => {
            const expiredCredentials = {
                serverName: 'test-server',
                token: { ...mockToken, expiresAt: Date.now() - 3600000 },
                clientId: 'test-client-id',
                tokenUrl: 'https://auth.example.com/token',
                updatedAt: Date.now(),
            };
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(expiredCredentials);
            vi.mocked(tokenStorage.isTokenExpired).mockReturnValue(true);
            vi.mocked(tokenStorage.deleteCredentials).mockResolvedValue(undefined);
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: false,
                status: 400,
                contentType: 'application/x-www-form-urlencoded',
                text: 'error=invalid_request&error_description=Invalid refresh token',
            }));
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBeNull();
            expect(tokenStorage.deleteCredentials).toHaveBeenCalledWith('test-server');
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('error', expect.stringContaining('Failed to refresh auth token'), expect.any(Error));
        });
        it('should return null for token without refresh capability', async () => {
            const tokenWithoutRefresh = {
                serverName: 'test-server',
                token: {
                    ...mockToken,
                    refreshToken: undefined,
                    expiresAt: Date.now() - 3600000,
                },
                clientId: 'test-client-id',
                tokenUrl: 'https://auth.example.com/token',
                updatedAt: Date.now(),
            };
            const tokenStorage = new MCPOAuthTokenStorage();
            vi.mocked(tokenStorage.getCredentials).mockResolvedValue(tokenWithoutRefresh);
            vi.mocked(tokenStorage.isTokenExpired).mockReturnValue(true);
            const authProvider = new MCPOAuthProvider();
            const result = await authProvider.getValidToken('test-server', mockConfig);
            expect(result).toBeNull();
        });
    });
    describe('PKCE parameter generation', () => {
        it('should generate valid PKCE parameters', async () => {
            // Test is implicit in the authenticate flow tests, but we can verify
            // the crypto mocks are called correctly
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', mockConfig);
            expect(crypto.randomBytes).toHaveBeenCalledWith(32); // code verifier
            expect(crypto.randomBytes).toHaveBeenCalledWith(16); // state
            expect(crypto.createHash).toHaveBeenCalledWith('sha256');
        });
    });
    describe('Authorization URL building', () => {
        it('should build correct authorization URL with all parameters', async () => {
            // Mock to capture the URL that would be opened
            let capturedUrl;
            mockOpenBrowserSecurely.mockImplementation((url) => {
                capturedUrl = url;
                return Promise.resolve();
            });
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', mockConfig, 'https://auth.example.com');
            expect(capturedUrl).toBeDefined();
            expect(capturedUrl).toContain('response_type=code');
            expect(capturedUrl).toContain('client_id=test-client-id');
            expect(capturedUrl).toContain('code_challenge=code_challenge_mock');
            expect(capturedUrl).toContain('code_challenge_method=S256');
            expect(capturedUrl).toContain('scope=read+write');
            expect(capturedUrl).toContain('resource=https%3A%2F%2Fauth.example.com');
            expect(capturedUrl).toContain('audience=https%3A%2F%2Fapi.example.com');
        });
        it('should correctly append parameters to an authorization URL that already has query params', async () => {
            // Mock to capture the URL that would be opened
            let capturedUrl;
            mockOpenBrowserSecurely.mockImplementation((url) => {
                capturedUrl = url;
                return Promise.resolve();
            });
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const configWithParamsInUrl = {
                ...mockConfig,
                authorizationUrl: 'https://auth.example.com/authorize?audience=1234',
            };
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', configWithParamsInUrl);
            const url = new URL(capturedUrl);
            expect(url.searchParams.get('audience')).toBe('1234');
            expect(url.searchParams.get('client_id')).toBe('test-client-id');
            expect(url.search.startsWith('?audience=1234&')).toBe(true);
        });
        it('should correctly append parameters to a URL with a fragment', async () => {
            // Mock to capture the URL that would be opened
            let capturedUrl;
            mockOpenBrowserSecurely.mockImplementation((url) => {
                capturedUrl = url;
                return Promise.resolve();
            });
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = {
                        writeHead: vi.fn(),
                        end: vi.fn(),
                    };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const configWithFragment = {
                ...mockConfig,
                authorizationUrl: 'https://auth.example.com/authorize#login',
            };
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', configWithFragment);
            const url = new URL(capturedUrl);
            expect(url.searchParams.get('client_id')).toBe('test-client-id');
            expect(url.hash).toBe('#login');
            expect(url.pathname).toBe('/authorize');
        });
        it('should use user-configured scopes over discovered scopes', async () => {
            let capturedUrl;
            mockOpenBrowserSecurely.mockImplementation((url) => {
                capturedUrl = url;
                return Promise.resolve();
            });
            const configWithUserScopes = {
                ...mockConfig,
                clientId: 'test-client-id',
                clientSecret: 'test-client-secret',
                scopes: ['user-scope'],
            };
            delete configWithUserScopes.authorizationUrl;
            delete configWithUserScopes.tokenUrl;
            const mockResourceMetadata = {
                authorization_servers: ['https://discovered.auth.com'],
            };
            const mockAuthServerMetadata = {
                authorization_endpoint: 'https://discovered.auth.com/authorize',
                token_endpoint: 'https://discovered.auth.com/token',
                scopes_supported: ['discovered-scope'],
            };
            mockFetch
                .mockResolvedValueOnce(createMockResponse({ ok: true, status: 200 }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockResourceMetadata),
                json: mockResourceMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockAuthServerMetadata),
                json: mockAuthServerMetadata,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = { writeHead: vi.fn(), end: vi.fn() };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', configWithUserScopes, 'https://api.example.com');
            expect(capturedUrl).toBeDefined();
            const url = new URL(capturedUrl);
            expect(url.searchParams.get('scope')).toBe('user-scope');
        });
        it('should use discovered scopes when no user-configured scopes are provided', async () => {
            let capturedUrl;
            mockOpenBrowserSecurely.mockImplementation((url) => {
                capturedUrl = url;
                return Promise.resolve();
            });
            const configWithoutScopes = {
                ...mockConfig,
                clientId: 'test-client-id',
                clientSecret: 'test-client-secret',
            };
            delete configWithoutScopes.scopes;
            delete configWithoutScopes.authorizationUrl;
            delete configWithoutScopes.tokenUrl;
            const mockResourceMetadata = {
                authorization_servers: ['https://discovered.auth.com'],
            };
            const mockAuthServerMetadata = {
                authorization_endpoint: 'https://discovered.auth.com/authorize',
                token_endpoint: 'https://discovered.auth.com/token',
                scopes_supported: ['discovered-scope-1', 'discovered-scope-2'],
            };
            mockFetch
                .mockResolvedValueOnce(createMockResponse({ ok: true, status: 200 }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockResourceMetadata),
                json: mockResourceMetadata,
            }))
                .mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockAuthServerMetadata),
                json: mockAuthServerMetadata,
            }));
            // Setup callback handler
            let callbackHandler;
            vi.mocked(http.createServer).mockImplementation((handler) => {
                callbackHandler = handler;
                return mockHttpServer;
            });
            mockHttpServer.listen.mockImplementation((port, callback) => {
                callback?.();
                setTimeout(() => {
                    const mockReq = {
                        url: '/oauth/callback?code=auth_code&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
                    };
                    const mockRes = { writeHead: vi.fn(), end: vi.fn() };
                    callbackHandler(mockReq, mockRes);
                }, 10);
            });
            // Mock token exchange
            mockFetch.mockResolvedValueOnce(createMockResponse({
                ok: true,
                contentType: 'application/json',
                text: JSON.stringify(mockTokenResponse),
                json: mockTokenResponse,
            }));
            const authProvider = new MCPOAuthProvider();
            await authProvider.authenticate('test-server', configWithoutScopes, 'https://api.example.com');
            expect(capturedUrl).toBeDefined();
            const url = new URL(capturedUrl);
            expect(url.searchParams.get('scope')).toBe('discovered-scope-1 discovered-scope-2');
        });
    });
    describe('issuer discovery conformance', () => {
        const registrationMetadata = {
            issuer: 'http://localhost:8888/realms/my-realm',
            authorization_endpoint: 'http://localhost:8888/realms/my-realm/protocol/openid-connect/auth',
            token_endpoint: 'http://localhost:8888/realms/my-realm/protocol/openid-connect/token',
            registration_endpoint: 'http://localhost:8888/realms/my-realm/clients-registrations/openid-connect',
        };
        it('falls back to path-based issuer when origin discovery fails', async () => {
            const authProvider = new MCPOAuthProvider();
            const providerWithAccess = authProvider;
            vi.spyOn(OAuthUtils, 'discoverAuthorizationServerMetadata').mockImplementation(async (issuer) => {
                if (issuer === 'http://localhost:8888/realms/my-realm') {
                    return registrationMetadata;
                }
                return null;
            });
            const result = await providerWithAccess.discoverAuthServerMetadataForRegistration('http://localhost:8888/realms/my-realm/protocol/openid-connect/auth');
            expect(vi.mocked(OAuthUtils.discoverAuthorizationServerMetadata).mock.calls).toEqual([
                ['http://localhost:8888'],
                ['http://localhost:8888/realms/my-realm'],
            ]);
            expect(result.issuerUrl).toBe('http://localhost:8888/realms/my-realm');
            expect(result.metadata).toBe(registrationMetadata);
        });
        it('trims versioned segments from authorization endpoints', async () => {
            const authProvider = new MCPOAuthProvider();
            const providerWithAccess = authProvider;
            const oktaMetadata = {
                issuer: 'https://auth.okta.local/oauth2/default',
                authorization_endpoint: 'https://auth.okta.local/oauth2/default/v1/authorize',
                token_endpoint: 'https://auth.okta.local/oauth2/default/v1/token',
                registration_endpoint: 'https://auth.okta.local/oauth2/default/v1/register',
            };
            const attempts = [];
            vi.spyOn(OAuthUtils, 'discoverAuthorizationServerMetadata').mockImplementation(async (issuer) => {
                attempts.push(issuer);
                if (issuer === 'https://auth.okta.local/oauth2/default') {
                    return oktaMetadata;
                }
                return null;
            });
            const result = await providerWithAccess.discoverAuthServerMetadataForRegistration('https://auth.okta.local/oauth2/default/v1/authorize');
            expect(attempts).toEqual([
                'https://auth.okta.local',
                'https://auth.okta.local/oauth2/default/v1',
                'https://auth.okta.local/oauth2/default',
            ]);
            expect(result.issuerUrl).toBe('https://auth.okta.local/oauth2/default');
            expect(result.metadata).toBe(oktaMetadata);
        });
    });
});
//# sourceMappingURL=oauth-provider.test.js.map