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
205 lines
6.4 KiB
TypeScript
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);
|
|
});
|
|
});
|