Files
promptfoo--promptfoo/test/commands/auth.test.ts
T
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

811 lines
28 KiB
TypeScript

import search from '@inquirer/search';
import { Command } from 'commander';
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { authCommand } from '../../src/commands/auth';
import { isNonInteractive } from '../../src/envars';
import { getUserEmail, setUserEmail } from '../../src/globalConfig/accounts';
import { cloudConfig } from '../../src/globalConfig/cloud';
import logger from '../../src/logger';
import { getDefaultTeam, getUserTeams } from '../../src/util/cloud';
import { fetchWithProxy } from '../../src/util/fetch/index';
import { openAuthBrowser } from '../../src/util/server';
import { createMockResponse, mockGlobal, stripAnsi } from '../util/utils';
vi.mock('@inquirer/search');
const mockCloudUser = {
id: '1',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockOrganization = {
id: '1',
name: 'Test Org',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockApp = {
id: '1',
name: 'Test App',
createdAt: new Date(),
updatedAt: new Date(),
url: 'https://app.example.com',
};
vi.mock('../../src/envars');
vi.mock('../../src/globalConfig/accounts');
vi.mock('../../src/globalConfig/cloud');
vi.mock('../../src/logger');
vi.mock('../../src/util/cloud');
vi.mock('../../src/util/fetch/index.ts');
vi.mock('../../src/util/server');
const mockFetch = vi.fn();
const restoreFetch = mockGlobal('fetch', mockFetch);
afterAll(() => {
restoreFetch();
});
describe('auth command', () => {
let program: Command;
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
program = new Command();
process.exitCode = undefined;
authCommand(program);
// Set up a basic mock that just returns the expected data
vi.mocked(cloudConfig.validateApiToken).mockResolvedValue({
user: mockCloudUser,
organization: mockOrganization,
app: mockApp,
hasActiveLicense: false,
});
});
describe('login', () => {
it('should set email in config after successful login with API key', async () => {
mockFetch.mockResolvedValueOnce(
createMockResponse({
ok: true,
body: { user: mockCloudUser },
}),
);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(setUserEmail).toHaveBeenCalledWith('test@example.com');
expect(cloudConfig.validateApiToken).toHaveBeenCalledWith('test-key', undefined);
expect(cloudConfig.saveValidatedApiToken).toHaveBeenCalledWith(
'test-key',
undefined,
mockCloudUser,
mockApp,
false,
);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully logged in'));
});
it('should prompt for browser opening when no API key is provided in interactive environment', async () => {
// Mock interactive environment
vi.mocked(isNonInteractive).mockReturnValue(false);
vi.mocked(cloudConfig.getAppUrl).mockReturnValue('https://www.promptfoo.app');
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test']);
expect(openAuthBrowser).toHaveBeenCalledWith(
'https://www.promptfoo.app/',
'https://www.promptfoo.app/welcome',
0, // BrowserBehavior.ASK
);
});
it('should exit with error when no API key is provided in non-interactive environment', async () => {
// Mock non-interactive environment (CI, cron, SSH without TTY, etc.)
vi.mocked(isNonInteractive).mockReturnValue(true);
vi.mocked(cloudConfig.getAppUrl).mockReturnValue('https://www.promptfoo.app');
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test']);
expect(logger.error).toHaveBeenCalledWith(
'Authentication required. Please set PROMPTFOO_API_KEY environment variable or run `promptfoo auth login` in an interactive environment.',
);
// Check that both info calls were made
const infoCalls = vi.mocked(logger.info).mock.calls;
const infoMessages = infoCalls.map((call) => stripAnsi(String(call[0])));
expect(infoCalls.length).toBeGreaterThanOrEqual(2);
expect(
infoMessages.some((message) =>
message.includes('Manual login URL: https://www.promptfoo.app/'),
),
).toBe(true);
expect(
infoMessages.some((message) =>
message.includes('After login, get your API token at: https://www.promptfoo.app/welcome'),
),
).toBe(true);
expect(process.exitCode).toBe(1);
expect(openAuthBrowser).not.toHaveBeenCalled();
});
it('should use custom host for browser opening when provided in interactive environment', async () => {
// Mock interactive environment
vi.mocked(isNonInteractive).mockReturnValue(false);
const customHost = 'https://custom.promptfoo.com';
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--host', customHost]);
expect(openAuthBrowser).toHaveBeenCalledWith(
'https://custom.promptfoo.com/',
'https://custom.promptfoo.com/welcome',
0, // BrowserBehavior.ASK
);
});
it('should use custom host when provided', async () => {
const customHost = 'https://custom-api.example.com';
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--host', customHost]);
expect(cloudConfig.validateApiToken).toHaveBeenCalledWith('test-key', customHost);
expect(cloudConfig.saveValidatedApiToken).toHaveBeenCalledWith(
'test-key',
customHost,
mockCloudUser,
mockApp,
false,
);
});
it('should handle login request failure', async () => {
vi.mocked(cloudConfig.validateApiToken).mockRejectedValueOnce(new Error('Bad Request'));
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Authentication failed: Bad Request'),
);
expect(process.exitCode).toBe(1);
});
it('should overwrite existing email in config after successful login', async () => {
const newCloudUser = { ...mockCloudUser, email: 'new@example.com' };
vi.mocked(getUserEmail).mockReturnValue('old@example.com');
vi.mocked(cloudConfig.validateApiToken).mockResolvedValueOnce({
user: newCloudUser,
organization: mockOrganization,
app: mockApp,
hasActiveLicense: false,
});
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(setUserEmail).toHaveBeenCalledWith('new@example.com');
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('Updating local email configuration'),
);
});
it('should handle non-Error objects in the catch block', async () => {
vi.mocked(cloudConfig.validateApiToken).mockImplementationOnce(function () {
// Intentionally throw a non-Error value to exercise defensive error-message handling.
throw 'String error message';
});
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Authentication failed: String error message'),
);
expect(process.exitCode).toBe(1);
// Reset exitCode
process.exitCode = 0;
});
it('should use --team flag to set specific team', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--team', 'security']);
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', '1');
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Security Team'));
});
it('should resolve --team across organizations when --org is omitted', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: 'org-2',
createdAt: '2024-01-02',
updatedAt: '2024-01-02',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--team', 'security']);
expect(cloudConfig.setCurrentOrganization).toHaveBeenCalledWith('org-2');
expect(cloudConfig.cacheTeams).toHaveBeenCalledWith([mockTeams[1]], 'org-2');
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', 'org-2');
expect(logger.warn).not.toHaveBeenCalled();
});
it('should use the custom host when resolving --team before saving API key login', async () => {
const customHost = 'https://api.promptfoo.example';
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: 'org-2',
createdAt: '2024-01-02',
updatedAt: '2024-01-02',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync([
'node',
'test',
'--api-key',
'test-key',
'--host',
customHost,
'--team',
'security',
]);
expect(getUserTeams).toHaveBeenCalledWith(customHost, 'test-key');
expect(cloudConfig.setCurrentOrganization).toHaveBeenCalledWith('org-2');
expect(cloudConfig.cacheTeams).toHaveBeenCalledWith([mockTeams[1]], 'org-2');
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', 'org-2');
});
it('should scope team selection and current organization to --org', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: 'org-2',
createdAt: '2024-01-02',
updatedAt: '2024-01-02',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--org', 'org-2']);
expect(cloudConfig.setCurrentOrganization).toHaveBeenCalledWith('org-2');
expect(cloudConfig.cacheTeams).toHaveBeenCalledWith([mockTeams[1]], 'org-2');
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', 'org-2');
});
it('should prefer an exact team name over a slug match when --org and --team are provided', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Slug Match',
slug: 'shared',
organizationId: 'org-2',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Shared',
slug: 'shared-name',
organizationId: 'org-2',
createdAt: '2024-01-02',
updatedAt: '2024-01-02',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync([
'node',
'test',
'--api-key',
'test-key',
'--org',
'org-2',
'--team',
'shared',
]);
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', 'org-2');
});
it('should log and persist the resolved organization when the default org has no teams', async () => {
const mockTeams = [
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: 'org-2',
createdAt: '2024-01-02',
updatedAt: '2024-01-02',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(cloudConfig.setCurrentOrganization).toHaveBeenCalledWith('org-2');
expect(cloudConfig.cacheTeams).toHaveBeenCalledWith([mockTeams[0]], 'org-2');
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', 'org-2');
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Organization:'));
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('org-2'));
});
it('should fail login when --org does not match any accessible team organization', async () => {
vi.mocked(getUserTeams).mockResolvedValue([
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
]);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--org', 'missing-org']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
"Authentication failed: Organization 'missing-org' not found in your accessible teams.",
),
);
expect(cloudConfig.saveValidatedApiToken).not.toHaveBeenCalled();
expect(setUserEmail).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});
it('should reject unknown --org when no teams are returned', async () => {
vi.mocked(getUserTeams).mockResolvedValue([]);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key', '--org', 'missing-org']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
"Authentication failed: Organization 'missing-org' not found in your accessible teams. Available organizations: 1",
),
);
expect(cloudConfig.saveValidatedApiToken).not.toHaveBeenCalled();
expect(setUserEmail).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});
it('should auto-select single team without prompting', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Only Team',
slug: 'only',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(search).not.toHaveBeenCalled();
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-1', '1');
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Only Team'));
});
it('should prompt for team selection when multiple teams exist in interactive mode', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security-alpha',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
vi.mocked(isNonInteractive).mockReturnValue(false);
vi.mocked(search).mockResolvedValue('team-2');
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(search).toHaveBeenCalledWith({
message: 'Select a team to use:',
source: expect.any(Function),
});
const source = vi.mocked(search).mock.calls[0]?.[0].source;
const signal = new AbortController().signal;
await expect(source?.(undefined, { signal })).resolves.toEqual([
expect.objectContaining({ name: 'Default', value: 'team-1', description: 'default' }),
expect.objectContaining({
name: 'Security Team',
value: 'team-2',
description: 'security-alpha',
}),
]);
await expect(source?.('sec', { signal })).resolves.toEqual([
expect.objectContaining({
name: 'Security Team',
value: 'team-2',
description: 'security-alpha',
}),
]);
await expect(source?.('alpha', { signal })).resolves.toEqual([
expect.objectContaining({
name: 'Security Team',
value: 'team-2',
description: 'security-alpha',
}),
]);
await expect(source?.('DEFAULT', { signal })).resolves.toEqual([
expect.objectContaining({ name: 'Default', value: 'team-1', description: 'default' }),
]);
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', '1');
});
it('should use default team with warning in non-interactive mode when multiple teams exist', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-02',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
vi.mocked(isNonInteractive).mockReturnValue(true);
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(search).not.toHaveBeenCalled();
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', '1');
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('You have access to 2 teams'),
);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('--team flag'));
});
it('should fall back to default team when user cancels interactive selection', async () => {
const mockTeams = [
{
id: 'team-1',
name: 'Default',
slug: 'default',
organizationId: '1',
createdAt: '2024-01-02',
updatedAt: '2024-01-01',
},
{
id: 'team-2',
name: 'Security Team',
slug: 'security',
organizationId: '1',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];
vi.mocked(getUserTeams).mockResolvedValue(mockTeams);
vi.mocked(isNonInteractive).mockReturnValue(false);
vi.mocked(search).mockRejectedValue(new Error('User cancelled'));
const loginCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'login');
await loginCmd?.parseAsync(['node', 'test', '--api-key', 'test-key']);
expect(cloudConfig.setCurrentTeamId).toHaveBeenCalledWith('team-2', '1');
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('(default)'));
});
});
describe('logout', () => {
it('should unset email and delete cloud config after logout', async () => {
vi.mocked(getUserEmail).mockReturnValue('test@example.com');
vi.mocked(cloudConfig.getApiKey).mockReturnValue('api-key');
const logoutCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'logout');
await logoutCmd?.parseAsync(['node', 'test']);
expect(cloudConfig.delete).toHaveBeenCalledWith();
expect(setUserEmail).toHaveBeenCalledWith('');
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully logged out'));
});
it('should show "already logged out" message when no session exists', async () => {
vi.mocked(getUserEmail).mockReturnValue(null);
vi.mocked(cloudConfig.getApiKey).mockReturnValue(undefined);
const logoutCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'logout');
await logoutCmd?.parseAsync(['node', 'test']);
expect(cloudConfig.delete).not.toHaveBeenCalled();
expect(setUserEmail).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining("You're already logged out"),
);
});
});
describe('whoami', () => {
it('should show user info when logged in', async () => {
vi.mocked(getUserEmail).mockReturnValue('test@example.com');
vi.mocked(cloudConfig.getApiKey).mockReturnValue('test-api-key');
vi.mocked(cloudConfig.getApiHost).mockReturnValue('https://api.example.com');
vi.mocked(cloudConfig.getAppUrl).mockReturnValue('https://app.example.com');
vi.mocked(getDefaultTeam).mockResolvedValueOnce({
id: 'team-1',
name: 'Default Team',
organizationId: 'org-1',
createdAt: '2023-01-01T00:00:00Z',
});
vi.mocked(fetchWithProxy).mockResolvedValueOnce(
createMockResponse({
ok: true,
body: {
user: mockCloudUser,
organization: mockOrganization,
},
}),
);
const whoamiCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'whoami');
await whoamiCmd?.parseAsync(['node', 'test']);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Currently logged in as:'));
});
it('should handle not logged in state', async () => {
// Reset logger mock before test
vi.mocked(logger.info).mockClear();
vi.mocked(getUserEmail).mockReturnValue(null);
vi.mocked(cloudConfig.getApiKey).mockReturnValue(undefined);
const whoamiCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'whoami');
await whoamiCmd?.parseAsync(['node', 'test']);
// Get the actual logged message
const infoMessages = vi.mocked(logger.info).mock.calls.map((call) => call[0]);
// Verify it contains our expected text
expect(infoMessages).toHaveLength(1);
expect(infoMessages[0]).toContain('Not logged in');
expect(infoMessages[0]).toContain('promptfoo auth login');
// No telemetry is recorded in this case (as per implementation)
});
it('should handle API error', async () => {
vi.mocked(getUserEmail).mockReturnValue('test@example.com');
vi.mocked(cloudConfig.getApiKey).mockReturnValue('test-api-key');
vi.mocked(cloudConfig.getApiHost).mockReturnValue('https://api.example.com');
vi.mocked(fetchWithProxy).mockResolvedValueOnce(
createMockResponse({
ok: false,
statusText: 'Internal Server Error',
}),
);
const whoamiCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'whoami');
await whoamiCmd?.parseAsync(['node', 'test']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
'Failed to get user info: Failed to fetch user info: Internal Server Error',
),
);
expect(process.exitCode).toBe(1);
process.exitCode = 0;
});
it('should handle failed API response with empty body', async () => {
vi.mocked(getUserEmail).mockReturnValue('test@example.com');
vi.mocked(cloudConfig.getApiKey).mockReturnValue('test-api-key');
vi.mocked(cloudConfig.getApiHost).mockReturnValue('https://api.example.com');
// Mock response with an empty body to exercise error-body fallback handling.
vi.mocked(fetchWithProxy).mockResolvedValueOnce(
createMockResponse({
ok: false,
statusText: 'Internal Server Error',
// Providing no body or an empty body
}),
);
const whoamiCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'whoami');
await whoamiCmd?.parseAsync(['node', 'test']);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
'Failed to get user info: Failed to fetch user info: Internal Server Error',
),
);
expect(process.exitCode).toBe(1);
// Reset exitCode
process.exitCode = 0;
});
it('should handle non-Error object in the catch block', async () => {
vi.mocked(getUserEmail).mockReturnValue('test@example.com');
vi.mocked(cloudConfig.getApiKey).mockReturnValue('test-api-key');
vi.mocked(cloudConfig.getApiHost).mockReturnValue('https://api.example.com');
// Intentionally throw a non-Error value to exercise defensive error-message handling.
vi.mocked(fetchWithProxy).mockImplementationOnce(function () {
throw 'String error from fetch';
});
const whoamiCmd = program.commands
.find((cmd) => cmd.name() === 'auth')
?.commands.find((cmd) => cmd.name() === 'whoami');
await whoamiCmd?.parseAsync(['node', 'test']);
expect(logger.error).toHaveBeenCalledWith('Failed to get user info: String error from fetch');
expect(process.exitCode).toBe(1);
// Reset exitCode
process.exitCode = 0;
});
});
});