Files
promptfoo--promptfoo/test/microsoftSharepoint.test.ts
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

328 lines
9.9 KiB
TypeScript

import fs from 'fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getEnvString } from '../src/envars';
import { fetchCsvFromSharepoint } from '../src/microsoftSharepoint';
import { fetchWithProxy } from '../src/util/fetch/index';
import { createMockResponse } from './util/utils';
vi.mock('../src/util/fetch/index.ts', () => ({
fetchWithProxy: vi.fn(),
}));
vi.mock('../src/envars', () => ({
getEnvString: vi.fn(),
}));
const mockReadFileSync = vi.hoisted(() => vi.fn());
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
...actual,
default: {
...actual,
readFileSync: mockReadFileSync,
},
readFileSync: mockReadFileSync,
};
});
vi.mock('fs/promises', () => ({
default: {
readFile: mockReadFileSync,
},
readFile: mockReadFileSync,
}));
// Mock the MSAL node module
const mockMsalClient = {
acquireTokenByClientCredential: vi.fn(),
};
vi.mock('@azure/msal-node', () => {
const ConfidentialClientApplication = vi.fn(function ConfidentialClientApplication() {
return mockMsalClient;
});
return { ConfidentialClientApplication };
});
describe('SharePoint Integration', () => {
const TEST_SHAREPOINT_URL =
'https://yourcompany.sharepoint.com/sites/yoursite/Shared%20Documents/test-cases.csv';
const TEST_BASE_URL = 'https://yourcompany.sharepoint.com';
const TEST_ACCESS_TOKEN = 'test-access-token-123';
beforeEach(() => {
vi.clearAllMocks();
// Setup default environment variables
vi.mocked(getEnvString).mockImplementation((key: string, defaultValue: string = '') => {
const values: Record<string, string> = {
SHAREPOINT_BASE_URL: TEST_BASE_URL,
SHAREPOINT_CLIENT_ID: 'test-client-id',
SHAREPOINT_TENANT_ID: 'test-tenant-id',
SHAREPOINT_CERT_PATH: '/path/to/cert.pem',
};
return values[key] || defaultValue;
});
// Mock certificate file reading with valid PEM content
const mockPemContent = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG+mRkSdMA0GCSqGSIb3DQEBCwUA
-----END CERTIFICATE-----`;
vi.mocked(fs.readFileSync).mockReturnValue(mockPemContent);
// Mock successful token acquisition
mockMsalClient.acquireTokenByClientCredential.mockResolvedValue({
accessToken: TEST_ACCESS_TOKEN,
});
});
afterEach(() => {
vi.resetAllMocks();
});
describe('fetchCsvFromSharepoint', () => {
it('should fetch and parse CSV data successfully', async () => {
const mockCsvData = 'language,input,__expected\nFrench,Hello world,icontains: bonjour';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
const result = await fetchCsvFromSharepoint(TEST_SHAREPOINT_URL);
expect(result).toEqual([
{
language: 'French',
input: 'Hello world',
__expected: 'icontains: bonjour',
},
]);
expect(fetchWithProxy).toHaveBeenCalledWith(
expect.stringContaining('/_api/web/GetFileByServerRelativeUrl'),
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${TEST_ACCESS_TOKEN}`,
Accept: 'text/csv',
}),
}),
);
});
it('should handle path encoding correctly', async () => {
const mockCsvData = 'header1,header2\nvalue1,value2';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
await fetchCsvFromSharepoint(TEST_SHAREPOINT_URL);
// Verify the API URL contains the file path (encodeURI may double-encode %20 as %2520)
const call = vi.mocked(fetchWithProxy).mock.calls[0][0] as string;
expect(call).toContain('/_api/web/GetFileByServerRelativeUrl');
expect(call).toContain('test-cases.csv');
});
it('should throw error when SHAREPOINT_BASE_URL is missing', async () => {
vi.mocked(getEnvString).mockImplementation((key: string, defaultValue: string = '') => {
if (key === 'SHAREPOINT_BASE_URL') {
return defaultValue;
}
return 'mock-value';
});
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'SHAREPOINT_BASE_URL environment variable is required',
);
});
it('should throw error on non-200 response', async () => {
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: false,
status: 403,
statusText: 'Forbidden',
}),
);
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'Failed to fetch CSV from SharePoint URL',
);
});
it('should throw error on 404 not found', async () => {
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: false,
status: 404,
statusText: 'Not Found',
}),
);
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'Failed to fetch CSV from SharePoint URL',
);
});
it('should handle CSV parsing errors gracefully', async () => {
const invalidCsvData = 'header1,header2\n"unclosed quote';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(invalidCsvData),
}),
);
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'Failed to parse CSV data from SharePoint',
);
});
it('should handle empty CSV correctly', async () => {
const mockCsvData = 'header1,header2\n';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
const result = await fetchCsvFromSharepoint(TEST_SHAREPOINT_URL);
expect(result).toEqual([]);
});
it('should handle CSV with special characters', async () => {
const mockCsvData = 'input,output\n"Hello, World!","Bonjour, Monde!"';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
const result = await fetchCsvFromSharepoint(TEST_SHAREPOINT_URL);
expect(result).toEqual([
{
input: 'Hello, World!',
output: 'Bonjour, Monde!',
},
]);
});
it('should normalize trailing slashes in base URL', async () => {
vi.mocked(getEnvString).mockImplementation((key: string, defaultValue: string = '') => {
const values: Record<string, string> = {
SHAREPOINT_BASE_URL: 'https://yourcompany.sharepoint.com/', // Trailing slash
SHAREPOINT_CLIENT_ID: 'test-client-id',
SHAREPOINT_TENANT_ID: 'test-tenant-id',
SHAREPOINT_CERT_PATH: '/path/to/cert.pem',
};
return values[key] || defaultValue;
});
const mockCsvData = 'header1,header2\nvalue1,value2';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
await fetchCsvFromSharepoint(TEST_SHAREPOINT_URL);
// Verify the URL doesn't have protocol://domain// pattern (but :// is ok)
const call = vi.mocked(fetchWithProxy).mock.calls[0][0] as string;
expect(call).not.toMatch(/com\/\//);
expect(call).toContain('/_api/web/');
});
});
describe('Authentication', () => {
// Note: Some auth validation tests are skipped due to client caching in getConfidentialClient
// These scenarios are covered by integration tests and actual usage
it('should throw error when token acquisition fails', async () => {
mockMsalClient.acquireTokenByClientCredential.mockResolvedValue(null);
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'Failed to acquire SharePoint access token',
);
});
it('should throw error when access token is missing', async () => {
mockMsalClient.acquireTokenByClientCredential.mockResolvedValue({
accessToken: null,
});
await expect(fetchCsvFromSharepoint(TEST_SHAREPOINT_URL)).rejects.toThrow(
'Failed to acquire SharePoint access token',
);
});
});
describe('URL Handling', () => {
it('should handle URLs without encoded spaces', async () => {
const urlWithSpaces =
'https://yourcompany.sharepoint.com/sites/yoursite/Shared Documents/test.csv';
const mockCsvData = 'header1,header2\nvalue1,value2';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
await fetchCsvFromSharepoint(urlWithSpaces);
// Should properly encode the path
expect(fetchWithProxy).toHaveBeenCalledWith(
expect.stringMatching(/Shared%20Documents/),
expect.any(Object),
);
});
it('should preserve special characters in file paths', async () => {
const urlWithSpecialChars =
'https://yourcompany.sharepoint.com/sites/yoursite/Files/test-data_v1.0.csv';
const mockCsvData = 'header1,header2\nvalue1,value2';
vi.mocked(fetchWithProxy).mockResolvedValue(
createMockResponse({
ok: true,
status: 200,
text: () => Promise.resolve(mockCsvData),
}),
);
const result = await fetchCsvFromSharepoint(urlWithSpecialChars);
expect(result).toBeDefined();
});
});
});