Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

1552 lines
50 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest';
import { cloudConfig } from '../../src/globalConfig/cloud';
import * as cloudModule from '../../src/util/cloud';
import {
ConfigPermissionError,
checkCloudPermissions,
getConfigFromCloud,
getDefaultTeam,
getEvalConfigFromCloud,
getPluginSeverityOverridesFromCloud,
getPoliciesFromCloud,
getProviderFromCloud,
makeRequest,
validateLinkedTargetId,
} from '../../src/util/cloud';
import { fetchWithProxy } from '../../src/util/fetch/index';
import { checkServerFeatureSupport } from '../../src/util/server';
import type { UnifiedConfig } from '../../src/types';
vi.mock('../../src/util/fetch/index.ts');
vi.mock('../../src/globalConfig/cloud');
vi.mock('../../src/util/server');
vi.mock('../../src/util/cloud', async () => {
const actual =
await vi.importActual<typeof import('../../src/util/cloud')>('../../src/util/cloud');
return {
...actual,
cloudCanBuildFormattedConfig: vi.fn().mockResolvedValue(true),
};
});
describe('cloud utils', () => {
const mockFetchWithProxy = vi.mocked(fetchWithProxy);
const mockCloudConfig = vi.mocked(cloudConfig);
const mockCheckServerFeatureSupport = vi.mocked(checkServerFeatureSupport);
let mockMakeRequest: MockInstance;
beforeEach(() => {
vi.resetAllMocks();
mockCloudConfig.getApiHost.mockReturnValue('https://api.example.com');
mockCloudConfig.getApiKey.mockReturnValue('test-api-key');
mockMakeRequest = vi.spyOn(cloudModule, 'makeRequest');
});
afterEach(() => {
mockMakeRequest?.mockRestore();
});
describe('makeRequest', () => {
it('should make request with correct URL and headers', async () => {
const path = 'test/path';
const method = 'POST';
const body = { data: 'test' };
await makeRequest(path, method, body);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'POST',
body: JSON.stringify(body),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should make GET request without body', async () => {
const path = 'test/path';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should handle undefined API key', async () => {
mockCloudConfig.getApiKey.mockReturnValue(undefined);
const path = 'test/path';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer undefined', 'Content-Type': 'application/json' },
});
});
it('should handle empty path', async () => {
const path = '';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/', {
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should handle API host without trailing slash', async () => {
mockCloudConfig.getApiHost.mockReturnValue('https://api.example.com');
const path = 'test/path';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/test/path',
expect.any(Object),
);
});
it('should handle API host with trailing slash', async () => {
mockCloudConfig.getApiHost.mockReturnValue('https://api.example.com');
const path = 'test/path';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/test/path',
expect.any(Object),
);
});
it('should handle path with leading slash', async () => {
const path = '/test/path';
const method = 'GET';
await makeRequest(path, method);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should handle complex request body', async () => {
const path = 'test/path';
const method = 'POST';
const body = {
string: 'test',
number: 123,
boolean: true,
array: [1, 2, 3],
nested: {
field: 'value',
},
};
await makeRequest(path, method, body);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'POST',
body: JSON.stringify(body),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should handle non-JSON body', async () => {
const path = 'test/path';
const method = 'POST';
const body = 'plain text body';
await makeRequest(path, method, body);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'POST',
body: JSON.stringify(body),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
it('should handle null/undefined body', async () => {
const path = 'test/path';
const method = 'POST';
await makeRequest(path, method, null);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'POST',
body: 'null',
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
await makeRequest(path, method, undefined);
expect(mockFetchWithProxy).toHaveBeenCalledWith('https://api.example.com/api/v1/test/path', {
method: 'POST',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
});
});
});
describe('getProviderFromCloud', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should fetch and parse provider successfully', async () => {
const mockProvider = {
config: {
id: 'test-provider',
label: 'Test Provider',
},
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockProvider),
ok: true,
} as Response);
const result = await getProviderFromCloud('test-provider');
expect(result).toEqual({ ...mockProvider.config });
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/providers/test-provider',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should throw error when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(getProviderFromCloud('test-provider')).rejects.toThrow(
'Could not fetch Provider test-provider from cloud. Cloud config is not enabled.',
);
});
it('should throw error when provider fetch fails', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Network error'));
await expect(getProviderFromCloud('test-provider')).rejects.toThrow(
'Failed to fetch provider from cloud: test-provider.',
);
});
it('should throw error when provider has no id', async () => {
const mockProvider = {
config: {
label: 'Test Provider',
// Missing id field
},
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve({ buildDate: '2025-03-011' }),
} as Response);
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockProvider),
} as Response);
await expect(getProviderFromCloud('test-provider')).rejects.toThrow(
'Failed to fetch provider from cloud: test-provider.',
);
});
});
describe('getConfigFromCloud', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should fetch unified config when formatted config is supported', async () => {
const mockUnifiedConfig = {
description: 'Test Config',
providers: ['test-provider'],
prompts: ['test prompt'],
tests: [{ vars: { input: 'test' } }],
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockUnifiedConfig),
ok: true,
} as Response);
const result = await getConfigFromCloud('test-config');
expect(result).toEqual(mockUnifiedConfig);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/redteam/configs/test-config/unified',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should fetch unified config with target', async () => {
const mockUnifiedConfig = {
description: 'Test Config',
providers: ['test-provider'],
prompts: ['test prompt'],
tests: [{ vars: { input: 'test' } }],
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockUnifiedConfig),
ok: true,
} as Response);
const result = await getConfigFromCloud('test-config', 'test-provider');
expect(result).toEqual(mockUnifiedConfig);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/redteam/configs/test-config/unified?providerId=test-provider',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should throw error when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(getConfigFromCloud('test-config')).rejects.toThrow(
'Could not fetch Config test-config from cloud. Cloud config is not enabled.',
);
});
it('should throw error when config fetch fails', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Network error'));
await expect(getConfigFromCloud('test-config')).rejects.toThrow(
'Failed to fetch config from cloud: test-config.',
);
});
it('should throw error when response is not ok', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
statusText: 'Not Found',
json: () => Promise.resolve({}),
} as Response);
await expect(getConfigFromCloud('test-config')).rejects.toThrow(
'Failed to fetch config from cloud: test-config.',
);
});
});
describe('getEvalConfigFromCloud', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should fetch and normalize eval config from cloud envelope', async () => {
const providerId = '12345678-1234-4234-8234-123456789abc';
const responseBody = {
config: {
id: 'config-id',
name: 'My Cloud Eval Config',
config: {
providerIds: [providerId],
prompts: [{ id: 'prompt-1', content: 'Hello {{name}}' }],
testCases: [{ vars: { name: 'World' } }],
delay: 250,
maxConcurrency: 2,
verbose: true,
},
},
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(responseBody),
} as Response);
const result = await getEvalConfigFromCloud('eval-config-id');
expect(result.description).toBe('My Cloud Eval Config');
expect(result.providers).toEqual([`promptfoo://provider/${providerId}`]);
expect(result.prompts).toEqual(['Hello {{name}}']);
expect(result.tests).toEqual([{ vars: { name: 'World' } }]);
expect(result.commandLineOptions).toEqual({
delay: 250,
maxConcurrency: 2,
verbose: true,
});
expect((result as any).providerIds).toBeUndefined();
expect((result as any).testCases).toBeUndefined();
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/configs/eval-config-id',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should default tests to an empty array when absent', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
config: {
description: 'Cloud Eval',
providers: ['promptfoo://provider/12345678-1234-4234-8234-123456789abc'],
prompts: ['Say hello'],
},
}),
} as Response);
const result = await getEvalConfigFromCloud('eval-config-id');
expect(result.tests).toEqual([]);
});
it('should throw error when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(getEvalConfigFromCloud('eval-config-id')).rejects.toThrow(
'Could not fetch Config eval-config-id from cloud. Cloud config is not enabled.',
);
});
it('should throw error when eval config fetch fails', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: () => Promise.resolve('Config not found'),
} as Response);
await expect(getEvalConfigFromCloud('eval-config-id')).rejects.toThrow(
'Failed to fetch config from cloud: Not Found',
);
});
});
describe('getPluginSeverityOverridesFromCloud', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should fetch and parse plugin severity overrides successfully', async () => {
const mockProvider = { pluginSeverityOverrideId: 'override-1' };
const mockOverride = {
id: 'override-1',
members: [
{ pluginId: 'plugin1', severity: 'HIGH' },
{ pluginId: 'plugin2', severity: 'LOW' },
],
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockProvider),
} as any);
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockOverride),
} as any);
const result = await getPluginSeverityOverridesFromCloud('test-provider');
expect(result).toEqual({
id: 'override-1',
severities: {
plugin1: 'HIGH',
plugin2: 'LOW',
},
});
});
it('should return null when no override ID exists', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({}),
} as any);
const result = await getPluginSeverityOverridesFromCloud('test-provider');
expect(result).toBeNull();
});
it('should throw error when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(getPluginSeverityOverridesFromCloud('test-provider')).rejects.toThrow(
'Could not fetch plugin severity overrides from cloud. Cloud config is not enabled.',
);
});
it('should throw error when provider fetch fails', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: () => Promise.resolve('Provider not found'),
} as any);
await expect(getPluginSeverityOverridesFromCloud('test-provider')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
it('should throw error when override fetch fails', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ pluginSeverityOverrideId: 'override-1' }),
} as any);
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: () => Promise.resolve('Override not found'),
} as any);
await expect(getPluginSeverityOverridesFromCloud('test-provider')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
it('should handle empty members array', async () => {
const mockProvider = { pluginSeverityOverrideId: 'override-1' };
const mockOverride = { id: 'override-1', members: [] };
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockProvider),
} as any);
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockOverride),
} as any);
const result = await getPluginSeverityOverridesFromCloud('test-provider');
expect(result).toEqual({
id: 'override-1',
severities: {},
});
});
it('should fetch and parse plugin severity overrides with one member', async () => {
const mockProvider = { pluginSeverityOverrideId: 'override-2' };
const mockOverride = {
id: 'override-2',
members: [{ pluginId: 'pluginX', severity: 'MEDIUM' }],
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockProvider),
} as any);
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockOverride),
} as any);
const result = await getPluginSeverityOverridesFromCloud('provider-2');
expect(result).toEqual({
id: 'override-2',
severities: {
pluginX: 'MEDIUM',
},
});
});
it('should handle override response with unexpected member fields', async () => {
const mockProvider = { pluginSeverityOverrideId: 'override-3' };
const mockOverride = {
id: 'override-3',
members: [
{ pluginId: 'pluginX', severity: 'LOW', extra: 'field' },
{ pluginId: 'pluginY', severity: 'HIGH' },
],
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockProvider),
} as any);
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockOverride),
} as any);
const result = await getPluginSeverityOverridesFromCloud('provider-3');
expect(result).toEqual({
id: 'override-3',
severities: {
pluginX: 'LOW',
pluginY: 'HIGH',
},
});
});
it('should throw error if first fetch throws synchronously', async () => {
mockFetchWithProxy.mockImplementationOnce(() => {
throw new Error('Synchronous fetch error');
});
await expect(getPluginSeverityOverridesFromCloud('provider-err')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
it('should throw error if second fetch throws synchronously', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ pluginSeverityOverrideId: 'override-err' }),
} as any);
mockFetchWithProxy.mockImplementationOnce(() => {
throw new Error('Synchronous fetch error 2');
});
await expect(getPluginSeverityOverridesFromCloud('provider-err2')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
it('should throw error if first fetch rejects', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Async fetch error'));
await expect(getPluginSeverityOverridesFromCloud('provider-err3')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
it('should throw error if second fetch rejects', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ pluginSeverityOverrideId: 'override-err4' }),
} as any);
mockFetchWithProxy.mockRejectedValueOnce(new Error('Async fetch error 2'));
await expect(getPluginSeverityOverridesFromCloud('provider-err4')).rejects.toThrow(
'Failed to fetch plugin severity overrides from cloud.',
);
});
});
describe('getPoliciesFromCloud', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should fetch and parse policies successfully with name attribute', async () => {
const mockPolicies = [
{
id: 'policy-1',
text: 'Policy text 1',
name: 'Policy Name 1',
severity: 'high',
},
{
id: 'policy-2',
text: 'Policy text 2',
name: 'Policy Name 2',
severity: 'medium',
},
{
id: 'policy-3',
text: 'Policy text 3',
name: 'Policy Name 3',
severity: 'low',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['policy-1', 'policy-2', 'policy-3'], 'team-123');
expect(result).toBeInstanceOf(Map);
expect(result.size).toBe(3);
expect(result.get('policy-1')).toEqual({
text: 'Policy text 1',
name: 'Policy Name 1',
severity: 'high',
});
expect(result.get('policy-2')).toEqual({
text: 'Policy text 2',
name: 'Policy Name 2',
severity: 'medium',
});
expect(result.get('policy-3')).toEqual({
text: 'Policy text 3',
name: 'Policy Name 3',
severity: 'low',
});
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/custom-policies/?id=policy-1&id=policy-2&id=policy-3&teamId=team-123',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should handle single policy with name attribute', async () => {
const mockPolicies = [
{
id: 'single-policy',
text: 'Single policy text',
name: 'Single Policy Name',
severity: 'critical',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['single-policy'], 'team-456');
expect(result).toBeInstanceOf(Map);
expect(result.size).toBe(1);
expect(result.get('single-policy')).toEqual({
text: 'Single policy text',
name: 'Single Policy Name',
severity: 'critical',
});
});
it('should handle policies with long names', async () => {
const longName =
'This is a very long policy name that contains many words and should be handled properly by the system';
const mockPolicies = [
{
id: 'long-name-policy',
text: 'Policy with long name',
name: longName,
severity: 'high',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['long-name-policy'], 'team-789');
expect(result.get('long-name-policy')).toEqual({
text: 'Policy with long name',
name: longName,
severity: 'high',
});
});
it('should handle policies with special characters in names', async () => {
const mockPolicies = [
{
id: 'special-chars-policy',
text: 'Policy text',
name: 'Policy with special chars: @#$%^&*()_+[]{}|;\':",.<>?/',
severity: 'medium',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['special-chars-policy'], 'team-special');
expect(result.get('special-chars-policy')).toEqual({
text: 'Policy text',
name: 'Policy with special chars: @#$%^&*()_+[]{}|;\':",.<>?/',
severity: 'medium',
});
});
it('should handle empty policy list', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
const result = await getPoliciesFromCloud([], 'team-empty');
expect(result).toBeInstanceOf(Map);
expect(result.size).toBe(0);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/custom-policies/?&teamId=team-empty',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should throw error when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(getPoliciesFromCloud(['policy-1'], 'team-123')).rejects.toThrow(
'Could not fetch policies from cloud. Cloud config is not enabled. Please run `promptfoo auth login` to login.',
);
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should throw error when response is not ok', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: () => Promise.resolve('Policies not found'),
} as Response);
await expect(getPoliciesFromCloud(['policy-1'], 'team-123')).rejects.toThrow(
'Failed to fetch policies from cloud.',
);
});
it('should throw error when fetch rejects', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Network error'));
await expect(getPoliciesFromCloud(['policy-1'], 'team-123')).rejects.toThrow(
'Failed to fetch policies from cloud.',
);
});
it('should throw error when response has invalid JSON', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.reject(new Error('Invalid JSON')),
} as Response);
await expect(getPoliciesFromCloud(['policy-1'], 'team-123')).rejects.toThrow(
'Failed to fetch policies from cloud.',
);
});
it('should handle multiple policies with different severities and names', async () => {
const mockPolicies = [
{
id: 'critical-policy',
text: 'Critical policy text',
name: 'Critical Security Policy',
severity: 'critical',
},
{
id: 'high-policy',
text: 'High policy text',
name: 'High Priority Policy',
severity: 'high',
},
{
id: 'medium-policy',
text: 'Medium policy text',
name: 'Medium Risk Policy',
severity: 'medium',
},
{
id: 'low-policy',
text: 'Low policy text',
name: 'Low Impact Policy',
severity: 'low',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(
['critical-policy', 'high-policy', 'medium-policy', 'low-policy'],
'team-all-severities',
);
expect(result.size).toBe(4);
expect(result.get('critical-policy')).toEqual({
text: 'Critical policy text',
name: 'Critical Security Policy',
severity: 'critical',
});
expect(result.get('high-policy')).toEqual({
text: 'High policy text',
name: 'High Priority Policy',
severity: 'high',
});
expect(result.get('medium-policy')).toEqual({
text: 'Medium policy text',
name: 'Medium Risk Policy',
severity: 'medium',
});
expect(result.get('low-policy')).toEqual({
text: 'Low policy text',
name: 'Low Impact Policy',
severity: 'low',
});
});
it('should properly encode multiple policy IDs in URL', async () => {
const policyIds = ['policy-1', 'policy-2', 'policy-3', 'policy-4', 'policy-5'];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await getPoliciesFromCloud(policyIds, 'team-multi');
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/custom-policies/?id=policy-1&id=policy-2&id=policy-3&id=policy-4&id=policy-5&teamId=team-multi',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should handle policies with empty names', async () => {
const mockPolicies = [
{
id: 'empty-name-policy',
text: 'Policy with empty name',
name: '',
severity: 'high',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['empty-name-policy'], 'team-empty-name');
expect(result.get('empty-name-policy')).toEqual({
text: 'Policy with empty name',
name: '',
severity: 'high',
});
});
it('should handle policies with unicode characters in names', async () => {
const mockPolicies = [
{
id: 'unicode-policy',
text: 'Unicode policy text',
name: 'Policy with unicode: 你好世界 🌍 مرحبا العالم',
severity: 'medium',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['unicode-policy'], 'team-unicode');
expect(result.get('unicode-policy')).toEqual({
text: 'Unicode policy text',
name: 'Policy with unicode: 你好世界 🌍 مرحبا العالم',
severity: 'medium',
});
});
it('should handle response with missing fields gracefully', async () => {
const mockPolicies = [
{
id: 'incomplete-policy',
text: 'Incomplete policy text',
// name field might be missing in some edge cases
severity: 'high',
},
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockPolicies),
} as Response);
const result = await getPoliciesFromCloud(['incomplete-policy'], 'team-incomplete');
// The function should still work even if name is undefined
expect(result.get('incomplete-policy')).toEqual({
text: 'Incomplete policy text',
name: undefined,
severity: 'high',
});
});
it('should handle team IDs with special characters', async () => {
const specialTeamId = 'team-123-@#$%';
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await getPoliciesFromCloud(['policy-1'], specialTeamId);
expect(mockFetchWithProxy).toHaveBeenCalledWith(
`https://api.example.com/api/v1/custom-policies/?id=policy-1&teamId=${specialTeamId}`,
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should handle HTTP error with detailed message', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
text: () => Promise.resolve('Database connection failed'),
} as Response);
await expect(getPoliciesFromCloud(['policy-1'], 'team-error')).rejects.toThrow(
'Failed to fetch policies from cloud.',
);
});
});
describe('getDefaultTeam', () => {
it('should return the oldest team', async () => {
const mockTeams = [
{ id: 'team-3', name: 'Team 3', createdAt: '2023-01-03T00:00:00Z' },
{ id: 'team-1', name: 'Team 1', createdAt: '2023-01-01T00:00:00Z' },
{ id: 'team-2', name: 'Team 2', createdAt: '2023-01-02T00:00:00Z' },
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTeams),
} as Response);
const result = await getDefaultTeam();
expect(result).toEqual({ id: 'team-1', name: 'Team 1', createdAt: '2023-01-01T00:00:00Z' });
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/users/me/teams',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should handle single team', async () => {
const mockTeams = [
{ id: 'team-single', name: 'Single Team', createdAt: '2023-01-01T00:00:00Z' },
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTeams),
} as Response);
const result = await getDefaultTeam();
expect(result).toEqual({
id: 'team-single',
name: 'Single Team',
createdAt: '2023-01-01T00:00:00Z',
});
});
it('should handle teams with same creation date', async () => {
const mockTeams = [
{ id: 'team-a', name: 'Team A', createdAt: '2023-01-01T00:00:00Z' },
{ id: 'team-b', name: 'Team B', createdAt: '2023-01-01T00:00:00Z' },
];
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTeams),
} as Response);
const result = await getDefaultTeam();
// Should return the first one when dates are the same
expect(result).toEqual({ id: 'team-a', name: 'Team A', createdAt: '2023-01-01T00:00:00Z' });
});
it('should throw error when request fails', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
statusText: 'Unauthorized',
} as Response);
await expect(getDefaultTeam()).rejects.toThrow('Failed to get user teams: Unauthorized');
});
it('should throw error when fetch throws', async () => {
mockFetchWithProxy.mockRejectedValueOnce(new Error('Network error'));
await expect(getDefaultTeam()).rejects.toThrow('Network error');
});
it('should handle empty teams array', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await expect(getDefaultTeam()).rejects.toThrow('No teams found for user');
});
});
describe('checkCloudPermissions', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
mockCheckServerFeatureSupport.mockResolvedValue(true);
});
it('should return early when cloud config is not enabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
expect(mockCheckServerFeatureSupport).not.toHaveBeenCalled();
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should return early with warning when no providers specified', async () => {
await expect(checkCloudPermissions({})).resolves.toBeUndefined();
expect(mockCheckServerFeatureSupport).not.toHaveBeenCalled();
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should return early when server feature is not supported', async () => {
mockCheckServerFeatureSupport.mockResolvedValue(false);
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
expect(mockCheckServerFeatureSupport).toHaveBeenCalledWith(
'config-permission-check-endpoint',
'2025-09-03T14:49:11Z',
);
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should pass when permissions check succeeds', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
} as Response);
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/permissions/check',
{
method: 'POST',
body: JSON.stringify({ config: { providers: ['test-provider'] } }),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should throw ConfigPermissionError when response is 403', async () => {
const errorData = {
errors: [
{ type: 'permission', id: 'access_denied', message: 'Access denied' },
{
type: 'permission',
id: 'insufficient_permissions',
message: 'Insufficient permissions',
},
],
};
mockFetchWithProxy.mockResolvedValue({
ok: false,
status: 403,
json: () => Promise.resolve(errorData),
} as Response);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
ConfigPermissionError,
);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
'Permission denied: permission access_denied: Access denied, permission insufficient_permissions: Insufficient permissions',
);
});
it('should throw ConfigPermissionError when response is 403 with single error', async () => {
const errorData = { error: 'Single error message' };
mockFetchWithProxy.mockResolvedValue({
ok: false,
status: 403,
json: () => Promise.resolve(errorData),
} as Response);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
ConfigPermissionError,
);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
'Permission denied: config unknown: Single error message',
);
});
it('should throw ConfigPermissionError when response is 403 with malformed JSON', async () => {
mockFetchWithProxy.mockResolvedValue({
ok: false,
status: 403,
json: () => Promise.reject(new Error('Invalid JSON')),
} as Response);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
ConfigPermissionError,
);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
'Permission denied: config unknown: Unknown error',
);
});
it('should log warning and continue for non-403 errors', async () => {
const errorData = {
errors: [{ type: 'server', id: 'internal_error', message: 'Server error' }],
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve(errorData),
} as Response);
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
});
it('should throw ConfigPermissionError when result contains errors', async () => {
const resultWithErrors = {
errors: [
{ type: 'config', id: 'validation_failed', message: 'Config validation failed' },
{ type: 'config', id: 'invalid_provider', message: 'Invalid provider' },
],
};
mockFetchWithProxy.mockResolvedValue({
ok: true,
json: () => Promise.resolve(resultWithErrors),
} as Response);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
ConfigPermissionError,
);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
'Not able to continue with config: config validation_failed: Config validation failed, config invalid_provider: Invalid provider',
);
});
it('should pass when result contains empty errors array', async () => {
const resultWithEmptyErrors = { errors: [] };
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(resultWithEmptyErrors),
} as Response);
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
});
it('should log warning and continue when server feature check throws', async () => {
mockCheckServerFeatureSupport.mockRejectedValue(new Error('Server check failed'));
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should log warning and continue when fetch throws non-ConfigPermissionError', async () => {
mockFetchWithProxy.mockRejectedValue(new Error('Network error'));
await expect(
checkCloudPermissions({ providers: ['test-provider'] }),
).resolves.toBeUndefined();
});
it('should re-throw ConfigPermissionError when makeRequest throws ConfigPermissionError', async () => {
const configError = new ConfigPermissionError('Permission denied');
mockFetchWithProxy.mockRejectedValue(configError);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
ConfigPermissionError,
);
await expect(checkCloudPermissions({ providers: ['test-provider'] })).rejects.toThrow(
'Permission denied',
);
});
it('should handle complex config object', async () => {
const complexConfig = {
providers: ['provider1', 'provider2'],
prompts: ['prompt1'],
tests: [{ vars: { input: 'test' } }],
redteam: { plugins: [{ id: 'plugin1' }] },
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
} as Response);
await expect(checkCloudPermissions(complexConfig)).resolves.toBeUndefined();
// Should strip tests and replace redteam with empty object
const expectedConfig = {
providers: ['provider1', 'provider2'],
prompts: ['prompt1'],
redteam: {},
};
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/permissions/check',
{
method: 'POST',
body: JSON.stringify({ config: expectedConfig }),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should strip heavy fields from config before sending', async () => {
const largeConfig: Partial<UnifiedConfig> = {
providers: ['provider1'],
prompts: ['prompt1'],
metadata: { configId: 'config-123', teamId: 'team-456' },
tests: Array.from({ length: 300 }, (_, i) => ({
vars: { input: `test-${i}` },
assert: [{ type: 'contains' as const, value: `expected-${i}` }],
})),
scenarios: [
{
config: [{ vars: { scenario: 'test' } }],
tests: [{ vars: { input: 'scenario-test' } }],
},
],
defaultTest: { vars: { default: 'value' } },
evaluateOptions: { maxConcurrency: 5 },
redteam: {
plugins: [{ id: 'plugin1' as const }, { id: 'plugin2' as const }],
strategies: [{ id: 'strategy1' as const }],
purpose: 'A very long purpose string',
},
};
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
} as Response);
await expect(checkCloudPermissions(largeConfig)).resolves.toBeUndefined();
const sentBody = JSON.parse((mockFetchWithProxy.mock.calls[0] as any[])[1].body as string);
const sentConfig = sentBody.config;
// Should keep providers, prompts, and metadata
expect(sentConfig.providers).toEqual(['provider1']);
expect(sentConfig.prompts).toEqual(['prompt1']);
expect(sentConfig.metadata).toEqual({ configId: 'config-123', teamId: 'team-456' });
// Should strip heavy fields
expect(sentConfig.tests).toBeUndefined();
expect(sentConfig.scenarios).toBeUndefined();
expect(sentConfig.defaultTest).toBeUndefined();
expect(sentConfig.evaluateOptions).toBeUndefined();
// Should replace redteam with empty object (preserving truthiness)
expect(sentConfig.redteam).toEqual({});
});
it('should handle config with undefined providers', async () => {
await expect(checkCloudPermissions({ providers: undefined })).resolves.toBeUndefined();
expect(mockCheckServerFeatureSupport).not.toHaveBeenCalled();
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should handle config with empty providers array', async () => {
mockFetchWithProxy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
} as Response);
await expect(checkCloudPermissions({ providers: [] })).resolves.toBeUndefined();
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/permissions/check',
{
method: 'POST',
body: JSON.stringify({ config: { providers: [] } }),
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
});
describe('validateLinkedTargetId', () => {
beforeEach(() => {
mockCloudConfig.isEnabled.mockReturnValue(true);
});
it('should accept valid linkedTargetId when cloud is enabled', async () => {
const validLinkedTargetId = 'promptfoo://provider/12345678-1234-1234-1234-123456789abc';
const mockProvider = {
config: {
id: 'test-provider',
label: 'Test Provider',
},
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockProvider),
ok: true,
} as Response);
await expect(validateLinkedTargetId(validLinkedTargetId)).resolves.toBeUndefined();
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/providers/12345678-1234-1234-1234-123456789abc',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
it('should warn but not fail when cloud is disabled', async () => {
mockCloudConfig.isEnabled.mockReturnValue(false);
const validLinkedTargetId = 'promptfoo://provider/12345678-1234-1234-1234-123456789abc';
await expect(validateLinkedTargetId(validLinkedTargetId)).resolves.toBeUndefined();
// Should not make API call when cloud is disabled
expect(mockFetchWithProxy).not.toHaveBeenCalled();
});
it('should throw error for invalid prefix', async () => {
const invalidPrefix = 'wrong://provider/12345678-1234-1234-1234-123456789abc';
const promise = validateLinkedTargetId(invalidPrefix);
await expect(promise).rejects.toThrow('Invalid linkedTargetId format');
await expect(promise).rejects.toThrow(invalidPrefix);
await expect(promise).rejects.toThrow('promptfoo://provider/');
await expect(promise).rejects.toThrow('To get a valid linkedTargetId:');
});
it('should throw error when target does not exist in cloud', async () => {
const validLinkedTargetId = 'promptfoo://provider/12345678-1234-1234-1234-123456789abc';
mockFetchWithProxy.mockRejectedValueOnce(new Error('Not found'));
const promise = validateLinkedTargetId(validLinkedTargetId);
await expect(promise).rejects.toThrow('linkedTargetId not found');
await expect(promise).rejects.toThrow(validLinkedTargetId);
await expect(promise).rejects.toThrow('Troubleshooting steps');
await expect(promise).rejects.toThrow('promptfoo auth status');
});
it('should throw error when API returns non-ok response', async () => {
const validLinkedTargetId = 'promptfoo://provider/12345678-1234-1234-1234-123456789abc';
mockFetchWithProxy.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
} as Response);
const promise = validateLinkedTargetId(validLinkedTargetId);
await expect(promise).rejects.toThrow('linkedTargetId not found');
await expect(promise).rejects.toThrow(validLinkedTargetId);
await expect(promise).rejects.toThrow('Troubleshooting steps');
});
it('should accept any ID format after valid prefix (defers to cloud)', async () => {
// Cloud API will validate the actual format - we just check prefix
const linkedTargetId = 'promptfoo://provider/any-id-format-here';
const mockProvider = {
config: {
id: 'test-provider',
},
};
mockFetchWithProxy.mockResolvedValueOnce({
json: () => Promise.resolve(mockProvider),
ok: true,
} as Response);
await expect(validateLinkedTargetId(linkedTargetId)).resolves.toBeUndefined();
expect(mockFetchWithProxy).toHaveBeenCalledWith(
'https://api.example.com/api/v1/providers/any-id-format-here',
{
method: 'GET',
body: undefined,
headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json' },
},
);
});
});
});