import { beforeEach, describe, expect, it, vi } from 'vitest'; import { assertProviderAccess, decideProviderAccess, ServerManagedProviderRequiredError, } from '@/lib/server/provider-access-policy'; describe('provider access policy', () => { it('rejects an unmanaged provider on a dedicated server deployment', () => { expect(decideProviderAccess('server', false)).toEqual({ allowed: false, reason: 'server-managed-provider-required', }); expect(() => assertProviderAccess('server', false)).toThrow(ServerManagedProviderRequiredError); }); it('allows a managed provider but never accepts client connection configuration', () => { expect(decideProviderAccess('server', true)).toEqual({ allowed: true, acceptClientConfiguration: false, }); }); it.each(['all', 'ops', 'learner'] as const)( 'keeps unmanaged provider compatibility for role=%s', (role) => { expect(decideProviderAccess(role, false)).toEqual({ allowed: true, acceptClientConfiguration: true, }); }, ); }); describe('provider-config deployment integration', () => { beforeEach(() => { vi.resetModules(); vi.unstubAllEnvs(); vi.stubEnv('NODE_ENV', 'test'); }); it('server + unmanaged rejects client key and base URL before model construction', async () => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server'); const { resolveApiKey, resolveBaseUrl } = await import('@/lib/server/provider-config'); // `vi.resetModules()` intentionally reloads the policy class together with // provider-config, so assert the stable public error contract rather than // identity against the pre-reset constructor. expect(() => resolveApiKey('unmanaged-provider', 'forged-client-key')).toThrow( /only permits providers configured by the server operator/, ); expect(() => resolveBaseUrl('unmanaged-provider', 'https://client.invalid/v1')).toThrow( /only permits providers configured by the server operator/, ); }); it('the shared LLM resolver rejects server-role BYOK before creating a provider client', async () => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server'); const { resolveModel } = await import('@/lib/server/resolve-model'); await expect( resolveModel({ modelString: 'caller-owned-provider:test-model', apiKey: 'forged-client-key', baseUrl: 'https://attacker.invalid/v1', }), ).rejects.toThrow(/only permits providers configured by the server operator/); }); it('server + managed ignores forged client values, including operator self-hosted URLs', async () => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server'); vi.stubEnv('OLLAMA_BASE_URL', 'http://ollama.operator.internal:11434'); const { resolveApiKey, resolveBaseUrl } = await import('@/lib/server/provider-config'); expect(resolveApiKey('ollama', 'forged-client-key')).toBe(''); expect(resolveBaseUrl('ollama', 'https://attacker.invalid/v1')).toBe( 'http://ollama.operator.internal:11434', ); }); it('server keeps the intrinsic local PDF parser without accepting caller connection data', async () => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server'); const { requireProviderAccess, resolvePDFApiKey, resolvePDFBaseUrl } = await import('@/lib/server/provider-config'); expect(requireProviderAccess('pdf', 'unpdf')).toEqual({ isServerConfigured: false, acceptClientConfiguration: false, }); expect(resolvePDFApiKey('unpdf', 'forged-client-key')).toBe(''); expect(resolvePDFBaseUrl('unpdf', 'https://attacker.invalid')).toBeUndefined(); }); it('the shared LLM resolver keeps operator self-hosted configuration authoritative', async () => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server'); vi.stubEnv('OLLAMA_BASE_URL', 'http://ollama.operator.internal:11434'); const { resolveModel } = await import('@/lib/server/resolve-model'); const resolved = await resolveModel({ modelString: 'ollama:llama3.3', apiKey: 'forged-client-key', baseUrl: 'https://attacker.invalid/v1', }); expect(resolved.apiKey).toBe(''); expect(resolved.baseUrl).toBe('http://ollama.operator.internal:11434'); }); it.each(['all', 'ops', 'learner'] as const)( 'role=%s keeps existing unmanaged BYOK resolution', async (role) => { vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', role); const { resolveApiKey, resolveBaseUrl } = await import('@/lib/server/provider-config'); expect(resolveApiKey('custom-provider', 'client-key')).toBe('client-key'); expect(resolveBaseUrl('custom-provider', 'https://client.example/v1')).toBe( 'https://client.example/v1', ); }, ); });