| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- /**
- * getProxyAgentForProvider Tests
- *
- * TDD: Tests written first, implementation follows
- */
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
- import type { Provider } from "@/types/provider";
- // Create mock objects outside the mock factory
- const mockAgent = {
- close: vi.fn().mockResolvedValue(undefined),
- destroy: vi.fn().mockResolvedValue(undefined),
- };
- const mockPool = {
- getAgent: vi.fn().mockResolvedValue({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|direct|h1",
- }),
- markUnhealthy: vi.fn(),
- evictEndpoint: vi.fn().mockResolvedValue(undefined),
- getPoolStats: vi.fn().mockReturnValue({
- cacheSize: 1,
- totalRequests: 1,
- cacheHits: 0,
- cacheMisses: 1,
- hitRate: 0,
- unhealthyAgents: 0,
- evictedAgents: 0,
- }),
- cleanup: vi.fn().mockResolvedValue(0),
- shutdown: vi.fn().mockResolvedValue(undefined),
- };
- // Mock the agent pool module
- vi.mock("@/lib/proxy-agent/agent-pool", () => ({
- getGlobalAgentPool: vi.fn(() => mockPool),
- resetGlobalAgentPool: vi.fn().mockResolvedValue(undefined),
- generateAgentCacheKey: vi.fn().mockImplementation((params) => {
- const url = new URL(params.endpointUrl);
- const proxy = params.proxyUrl || "direct";
- const protocol = params.enableHttp2 ? "h2" : "h1";
- return `${url.origin}|${proxy}|${protocol}`;
- }),
- AgentPoolImpl: vi.fn(),
- }));
- // Import after mock setup
- import { getProxyAgentForProvider, type ProxyConfigWithCacheKey } from "@/lib/proxy-agent";
- describe("getProxyAgentForProvider", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- // Reset default mock return value
- mockPool.getAgent.mockResolvedValue({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|direct|h1",
- });
- });
- afterEach(() => {
- vi.clearAllMocks();
- });
- describe("direct connection (no proxy)", () => {
- it("should return null when provider has no proxy configured", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: null,
- proxyFallbackToDirect: false,
- };
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).toBeNull();
- expect(mockPool.getAgent).not.toHaveBeenCalled();
- });
- it("should return null when proxyUrl is empty string", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "",
- proxyFallbackToDirect: false,
- };
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).toBeNull();
- });
- it("should return null when proxyUrl is whitespace only", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: " ",
- proxyFallbackToDirect: false,
- };
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).toBeNull();
- });
- });
- describe("with proxy configured", () => {
- it("should return ProxyConfig with cacheKey for HTTP proxy", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "http://proxy.example.com:8080",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|http://proxy.example.com:8080|h1",
- });
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).not.toBeNull();
- expect(result?.cacheKey).toBe("https://api.anthropic.com|http://proxy.example.com:8080|h1");
- expect(result?.fallbackToDirect).toBe(false);
- expect(result?.http2Enabled).toBe(false);
- expect(mockPool.getAgent).toHaveBeenCalledWith({
- endpointUrl: "https://api.anthropic.com/v1/messages",
- proxyUrl: "http://proxy.example.com:8080",
- enableHttp2: false,
- });
- });
- it("should return ProxyConfig with HTTP/2 enabled", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "http://proxy.example.com:8080",
- proxyFallbackToDirect: true,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|http://proxy.example.com:8080|h2",
- });
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- true
- );
- expect(result).not.toBeNull();
- expect(result?.http2Enabled).toBe(true);
- expect(result?.fallbackToDirect).toBe(true);
- expect(mockPool.getAgent).toHaveBeenCalledWith({
- endpointUrl: "https://api.anthropic.com/v1/messages",
- proxyUrl: "http://proxy.example.com:8080",
- enableHttp2: true,
- });
- });
- it("should handle SOCKS proxy", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "socks5://proxy.example.com:1080",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|socks5://proxy.example.com:1080|h1",
- });
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).not.toBeNull();
- expect(result?.cacheKey).toContain("socks5://");
- });
- it("should disable HTTP/2 for SOCKS proxy even when requested", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "socks5://proxy.example.com:1080",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|socks5://proxy.example.com:1080|h1",
- });
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- true // Request HTTP/2
- );
- expect(result).not.toBeNull();
- expect(result?.http2Enabled).toBe(false); // Should be false for SOCKS
- });
- it("should mask proxy URL in result", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "http://user:[email protected]:8080",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|http://user:[email protected]:8080|h1",
- });
- const result = await getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).not.toBeNull();
- // proxyUrl should be masked (password hidden)
- expect(result?.proxyUrl).not.toContain("password");
- expect(result?.proxyUrl).toContain("***");
- });
- });
- describe("ProviderProxyConfig interface", () => {
- it("should work with minimal ProviderProxyConfig", async () => {
- const config = {
- id: 1,
- proxyUrl: "http://proxy.example.com:8080",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|http://proxy.example.com:8080|h1",
- });
- const result = await getProxyAgentForProvider(
- config,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).not.toBeNull();
- expect(result?.cacheKey).toBeDefined();
- });
- it("should work with optional name field", async () => {
- const config = {
- id: 1,
- name: "My Proxy",
- proxyUrl: "http://proxy.example.com:8080",
- proxyFallbackToDirect: true,
- };
- mockPool.getAgent.mockResolvedValueOnce({
- agent: mockAgent,
- isNew: true,
- cacheKey: "https://api.anthropic.com|http://proxy.example.com:8080|h1",
- });
- const result = await getProxyAgentForProvider(
- config,
- "https://api.anthropic.com/v1/messages",
- false
- );
- expect(result).not.toBeNull();
- expect(result?.fallbackToDirect).toBe(true);
- });
- });
- describe("error handling", () => {
- it("should throw on invalid proxy URL", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "not-a-valid-url",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockRejectedValueOnce(new Error("Invalid URL"));
- await expect(
- getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- )
- ).rejects.toThrow();
- });
- it("should throw on unsupported proxy protocol", async () => {
- const provider: Partial<Provider> = {
- id: 1,
- name: "Test Provider",
- proxyUrl: "ftp://proxy.example.com:21",
- proxyFallbackToDirect: false,
- };
- mockPool.getAgent.mockRejectedValueOnce(new Error("Unsupported proxy protocol"));
- await expect(
- getProxyAgentForProvider(
- provider as Provider,
- "https://api.anthropic.com/v1/messages",
- false
- )
- ).rejects.toThrow();
- });
- });
- });
|