Files
crank/apps/ui/tests/e2e/api-keys.spec.js
T

308 lines
13 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { browserJson, createAgent, getCurrentWorkspace, login, localized, uniqueName } = require('./helpers');
test('api keys page opens create key flow', async ({ page }) => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
await login(page);
const workspace = await getCurrentWorkspace(page);
await createAgent(page, workspace.id, {
slug: uniqueName('playwright_keys_agent'),
display_name: 'Playwright Keys Agent',
description: 'Agent for API key page smoke test.',
instructions: {},
tool_selection_policy: {},
});
await page.goto('/api-keys');
await expect(page.locator('.page-title')).toHaveText(localized('Agent Keys', 'Ключи агентов'));
await expect(page.locator('[data-testid="machine-access-summary"]')).toHaveCount(0);
await expect(page.locator('#machine-access-note')).toHaveCount(0);
await expect(page.locator('[data-i18n="apikeys.agent.subtitle"]')).toContainText(
localized('Select the AI agent this key is issued for', 'Выберите AI-агента')
);
await expect(page.locator('#agent-select')).not.toBeDisabled();
await expect(page.locator('#btn-create-key')).toBeEnabled();
await page.locator('#btn-create-key').click();
await expect(page.locator('#modal-create')).toHaveClass(/open/);
await expect(page.locator('.modal-title')).toHaveText(localized('Create MCP client key', 'Создать ключ MCP-клиента'));
await expect(page.locator('[data-scope="read"]')).toBeChecked();
await expect(page.locator('[data-scope="write"]')).toBeChecked();
await page.locator('#new-key-name').fill(`playwright-${Date.now()}`);
await page.locator('#modal-confirm-btn').click();
await expect(page.locator('#modal-reveal-body')).toContainText(localized('Copy this key now', 'Скопируйте этот ключ сейчас'));
await expect(page.locator('#reveal-key-value')).toContainText('crk_');
const config = page.getByTestId('onboarding-connection-config');
await expect(config).toBeVisible();
await config.getByRole('button', { name: localized('Copy configuration', 'Скопировать конфигурацию') }).first().click();
await expect(page.locator('#reveal-key-value')).toHaveText('');
await expect(config).toBeHidden();
await expect(page.locator('#onboarding-connection-clients')).toBeEmpty();
await page.locator('#modal-done-btn').click();
await expect(page.locator('#modal-create')).not.toHaveClass(/open/);
await expect(page.locator('#reveal-key-value')).toHaveText('');
await page.locator('#key-kind-approval').click();
await expect(page.locator('#key-kind-hint')).toContainText(
localized('human confirmation interface', 'человек подтверждает действие')
);
await expect(page.locator('#btn-create-key')).toContainText(
localized('Create approval key', 'Создать ключ подтверждения')
);
await page.locator('#btn-create-key').click();
await expect(page.locator('.modal-title')).toHaveText(localized('Create approval key', 'Создать ключ подтверждения'));
await expect(page.locator('#approval-key-warning')).toContainText(
localized('Do not pass this key to an LLM', 'Не передавайте этот ключ LLM')
);
await page.locator('#new-key-name').fill(`playwright-approval-${Date.now()}`);
await page.locator('#modal-confirm-btn').click();
await expect(page.locator('#reveal-key-value')).toContainText('crk_appr_');
await expect(page.locator('#onboarding-connection-config')).toBeHidden();
const approvalRawKey = await page.locator('#reveal-key-value').textContent();
await page.goto('/agents');
await page.goBack();
await expect(page.locator('#reveal-key-value')).not.toContainText(approvalRawKey || 'crk_appr_');
});
test('failed clipboard write preserves one-time key material', async ({ page }) => { // community-scope: allow=one-time-token
await login(page);
const workspace = await getCurrentWorkspace(page);
await createAgent(page, workspace.id, {
slug: uniqueName('playwright_clipboard_agent'),
display_name: 'Playwright Clipboard Agent',
description: 'Agent for clipboard failure recovery.',
instructions: {},
tool_selection_policy: {},
});
await page.addInitScript(() => {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: () => Promise.reject(new Error('clipboard denied')) },
});
});
await page.goto('/api-keys');
await page.locator('#btn-create-key').click();
await page.locator('#new-key-name').fill(uniqueName('clipboard_failure'));
await page.locator('#modal-confirm-btn').click();
await expect(page.locator('#reveal-key-value')).toHaveText(/^crk_/);
const rawKey = await page.locator('#reveal-key-value').textContent();
expect(rawKey).toMatch(/^crk_/);
await page.locator('#copy-key-btn').click();
await expect(page.locator('#reveal-key-value')).toHaveText(rawKey);
await expect(page.locator('#modal-reveal-body')).toContainText(
localized('Copy this key now', 'Скопируйте ключ сейчас')
);
const config = page.getByTestId('onboarding-connection-config');
await expect(config).toBeVisible();
await config.getByRole('button', { name: localized('Copy configuration', 'Скопировать конфигурацию') }).first().click();
await expect(page.locator('#reveal-key-value')).toHaveText(rawKey);
await expect(config).toBeVisible();
await expect(page.locator('#onboarding-connection-clients')).not.toBeEmpty();
});
test('ambiguous create failure reconciles metadata before deliberate retry', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
await createAgent(page, workspace.id, {
slug: uniqueName('playwright_ambiguous_agent'),
display_name: 'Playwright Ambiguous Agent',
description: 'Agent for ambiguous key creation recovery.',
instructions: {},
tool_selection_policy: {},
});
let failCreate = true;
await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys$/, async (route) => {
if (route.request().method() === 'POST' && failCreate) {
failCreate = false;
await route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ error: { code: 'key_create_outcome_unknown', message: 'unknown' } }),
});
return;
}
await route.continue();
});
await page.goto('/api-keys');
await page.locator('#btn-create-key').click();
await page.locator('#new-key-name').fill(uniqueName('ambiguous_key'));
await page.locator('#modal-confirm-btn').click();
await expect(page.getByTestId('ambiguous-create-warning')).toBeVisible();
await expect(page.locator('#modal-confirm-btn')).toBeDisabled();
await page.locator('#ambiguous-create-retry-btn').click();
await expect(page.getByTestId('ambiguous-create-warning')).toBeHidden();
await expect(page.locator('#modal-confirm-btn')).toBeEnabled();
});
test('stale create response reconciles metadata without changing a newer modal generation', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
const agent = await createAgent(page, workspace.id, {
slug: uniqueName('playwright_keys_stale_agent'),
display_name: 'Playwright Keys Stale Agent',
description: 'Agent for API key stale response test.',
instructions: {},
tool_selection_policy: {},
});
let releaseFirstCreate;
let releaseSecondCreate;
const firstCreateReleased = new Promise((resolve) => {
releaseFirstCreate = resolve;
});
const secondCreateReleased = new Promise((resolve) => {
releaseSecondCreate = resolve;
});
let createCount = 0;
await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys$/, async (route) => {
if (route.request().method() !== 'POST') {
await route.continue();
return;
}
const requestNumber = ++createCount;
await (requestNumber === 1 ? firstCreateReleased : secondCreateReleased);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
secret: requestNumber === 1
? 'crk_ui_stale_canary_should_not_render'
: 'crk_ui_current_canary_may_render',
api_key: {
api_key: {
id: requestNumber === 1 ? 'pk_ui_stale' : 'pk_ui_current',
workspace_id: workspace.id,
agent_id: agent.agent_id,
key_kind: 'mcp_client',
name: requestNumber === 1 ? 'ui-stale-key' : 'ui-current-key',
prefix: requestNumber === 1 ? 'crk_ui_stale' : 'crk_ui_current',
scopes: ['read'],
status: 'active',
created_at: '2026-08-15T00:00:00Z',
last_used_at: null,
expires_at: null,
allowed_origins: [],
},
},
}),
});
});
await page.goto('/api-keys');
await page.locator('#btn-create-key').click();
await page.locator('#new-key-name').fill(uniqueName('ui_stale_key'));
await page.locator('#modal-confirm-btn').click();
await expect(page.locator('#modal-confirm-btn')).toBeDisabled();
await page.locator('#modal-cancel-btn').click();
await expect(page.locator('#modal-create')).not.toHaveClass(/open/);
await page.locator('#btn-create-key').click();
await page.locator('#new-key-name').fill(uniqueName('ui_current_key'));
await page.locator('#modal-confirm-btn').click();
const confirm = page.locator('#modal-confirm-btn');
await expect(confirm).toBeDisabled();
await expect(confirm).toContainText(localized('Creating', 'Создание'));
releaseFirstCreate();
await expect(page.getByTestId('onboarding-key-lost')).toBeVisible();
await expect(confirm).toBeDisabled();
await expect(confirm).toContainText(localized('Creating', 'Создание'));
releaseSecondCreate();
await expect(page.locator('#modal-reveal-body')).toBeVisible();
await expect(page.locator('#reveal-key-value')).not.toContainText('crk_ui_stale_canary_should_not_render');
await expect(page.locator('#reveal-key-value')).toContainText('crk_ui_current_canary_may_render');
});
test('duplicate key revoke and delete clicks send one request while the first request is pending', async ({ page }) => {
await page.addInitScript(() => {
window.confirm = () => true;
});
await login(page);
const workspace = await getCurrentWorkspace(page);
const agent = await createAgent(page, workspace.id, {
slug: uniqueName('playwright_key_mutation_agent'),
display_name: 'Playwright Key Mutation Agent',
description: 'Agent for duplicate key mutation guards.',
instructions: {},
tool_selection_policy: {},
});
const revokeName = uniqueName('revoke_once');
const deleteName = uniqueName('delete_once');
await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys`,
{ name: revokeName, key_kind: 'mcp_client', scopes: ['read'] },
);
const revokedKey = await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys`,
{ name: deleteName, key_kind: 'mcp_client', scopes: ['read'] },
);
const revokedKeyId = revokedKey.api_key.api_key.id;
await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys/${encodeURIComponent(revokedKeyId)}/revoke`,
{},
[204],
);
let releaseRevoke;
const revokeReleased = new Promise((resolve) => {
releaseRevoke = resolve;
});
let revokeRequests = 0;
let releaseDelete;
const deleteReleased = new Promise((resolve) => {
releaseDelete = resolve;
});
let deleteRequests = 0;
await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys\/[^/]+(?:\/revoke)?$/, async (route) => {
const method = route.request().method();
if (method === 'POST' && /\/revoke$/.test(route.request().url())) {
revokeRequests += 1;
await revokeReleased;
await route.fulfill({ status: 204 });
return;
}
if (method === 'DELETE') {
deleteRequests += 1;
await deleteReleased;
await route.fulfill({ status: 204 });
return;
}
await route.continue();
});
await page.goto('/api-keys');
await page.locator('#agent-select').selectOption(agent.agent_id);
await expect(page.locator('#keys-tbody')).toContainText(revokeName);
await expect(page.locator('#keys-tbody')).toContainText(deleteName);
await page.evaluate((name) => {
const row = Array.from(document.querySelectorAll('#keys-tbody tr'))
.find((candidate) => candidate.textContent.includes(name));
const button = row.querySelector('[title="Revoke key"]');
button.click();
button.click();
}, revokeName);
await expect.poll(() => revokeRequests).toBe(1);
releaseRevoke();
await expect(page.locator('#keys-tbody')).toContainText(revokeName);
const deleteRow = page.locator('#keys-tbody tr').filter({ hasText: deleteName }).first();
const deleteButton = deleteRow.locator('[title="Delete"]');
await expect(deleteRow).toBeVisible();
await expect(deleteButton).toBeEnabled();
await deleteButton.evaluate((button) => {
button.click();
button.click();
});
await expect.poll(() => deleteRequests).toBe(1);
releaseDelete();
await expect(page.locator('#keys-tbody')).toContainText(deleteName);
});