Files
promptfoo--promptfoo/test/googleSheets.optionalDependency.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

205 lines
6.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
const SHEET_URL = 'https://docs.google.com/spreadsheets/d/1234567890/edit';
describe('Google Sheets optional dependency', () => {
afterEach(() => {
vi.doUnmock('@googleapis/sheets');
vi.doUnmock('../src/util/fetch/index');
vi.doUnmock('../src/util/packageImportErrors');
vi.resetModules();
});
it('explains how to install the authenticated Sheets dependency', async () => {
vi.doMock('@googleapis/sheets', () => {
throw new Error('Cannot find package @googleapis/sheets');
});
vi.doMock('../src/util/packageImportErrors', () => ({
isMissingPackageImportError: () => true,
}));
const { fetchCsvFromGoogleSheetAuthenticated } = await import('../src/googleSheets');
const fetchPromise = fetchCsvFromGoogleSheetAuthenticated(SHEET_URL);
await expect(fetchPromise).rejects.toThrow(
/The @googleapis\/sheets package is required for authenticated Google Sheets access\.[\s\S]*npm install @googleapis\/sheets/,
);
});
it('keeps public-sheet CSV reads working when the access probe fails', async () => {
const fetchWithProxy = vi
.fn()
.mockRejectedValueOnce(new Error('probe failed'))
.mockResolvedValueOnce({
status: 200,
text: async () => 'header\nvalue',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
const { fetchCsvFromGoogleSheet } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheet(SHEET_URL)).resolves.toEqual([{ header: 'value' }]);
expect(fetchWithProxy).toHaveBeenCalledTimes(2);
});
it('parses CSV exports whose first header looks like an HTML tag', async () => {
const fetchWithProxy = vi.fn().mockResolvedValueOnce({
status: 200,
headers: {
get: () => 'text/csv; charset=utf-8',
},
text: async () => '<html>,label\nliteral,value',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
const { fetchCsvFromGoogleSheetUnauthenticated } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheetUnauthenticated(SHEET_URL)).resolves.toEqual([
{ '<html>': 'literal', label: 'value' },
]);
});
it('labels login-page export errors for authentication fallback classification', async () => {
const fetchWithProxy = vi.fn().mockResolvedValueOnce({
status: 200,
headers: {
get: () => 'text/html; charset=utf-8',
},
text: async () => '<!doctype html><html><body>Sign in</body></html>',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
const { fetchCsvFromGoogleSheetUnauthenticated } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheetUnauthenticated(SHEET_URL)).rejects.toMatchObject({
name: 'GoogleSheetAuthenticationRequiredError',
});
});
it('falls back to authenticated sheet access when the probe and CSV export both fail', async () => {
const fetchWithProxy = vi
.fn()
.mockRejectedValueOnce(new Error('probe failed'))
.mockResolvedValueOnce({
status: 403,
text: async () => 'forbidden',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
vi.doMock('@googleapis/sheets', () => ({
auth: {
GoogleAuth: class {},
},
sheets: () => ({
spreadsheets: {
get: vi.fn().mockResolvedValue({
data: {
sheets: [{ properties: { title: 'Sheet1' } }],
},
}),
values: {
get: vi.fn().mockResolvedValue({
data: {
values: [['header'], ['value']],
},
}),
},
},
}),
}));
const { fetchCsvFromGoogleSheet } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheet(SHEET_URL)).resolves.toEqual([{ header: 'value' }]);
expect(fetchWithProxy).toHaveBeenCalledTimes(2);
});
it('falls back to authenticated access when CSV export returns a login page', async () => {
const fetchWithProxy = vi
.fn()
.mockRejectedValueOnce(new Error('probe failed'))
.mockResolvedValueOnce({
status: 200,
headers: {
get: () => 'text/html; charset=utf-8',
},
text: async () => '<!doctype html><html><body>Sign in</body></html>',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
vi.doMock('@googleapis/sheets', () => ({
auth: {
GoogleAuth: class {},
},
sheets: () => ({
spreadsheets: {
get: vi.fn().mockResolvedValue({
data: {
sheets: [{ properties: { title: 'Sheet1' } }],
},
}),
values: {
get: vi.fn().mockResolvedValue({
data: {
values: [['header'], ['value']],
},
}),
},
},
}),
}));
const { fetchCsvFromGoogleSheet } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheet(SHEET_URL)).resolves.toEqual([{ header: 'value' }]);
expect(fetchWithProxy).toHaveBeenCalledTimes(2);
});
it('falls back to authenticated access when the access probe masks a login page', async () => {
const fetchWithProxy = vi
.fn()
.mockResolvedValueOnce({
ok: true,
status: 200,
})
.mockResolvedValueOnce({
status: 200,
headers: {
get: () => 'text/html; charset=utf-8',
},
text: async () => '<!doctype html><html><body>Sign in</body></html>',
});
vi.doMock('../src/util/fetch/index', () => ({ fetchWithProxy }));
vi.doMock('@googleapis/sheets', () => ({
auth: {
GoogleAuth: class {},
},
sheets: () => ({
spreadsheets: {
get: vi.fn().mockResolvedValue({
data: {
sheets: [{ properties: { title: 'Sheet1' } }],
},
}),
values: {
get: vi.fn().mockResolvedValue({
data: {
values: [['header'], ['value']],
},
}),
},
},
}),
}));
const { fetchCsvFromGoogleSheet } = await import('../src/googleSheets');
await expect(fetchCsvFromGoogleSheet(SHEET_URL)).resolves.toEqual([{ header: 'value' }]);
expect(fetchWithProxy).toHaveBeenCalledTimes(2);
});
});