Files
crank/apps/ui/tests/e2e/secrets.spec.js
T

218 lines
8.5 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { browserJson, getCurrentWorkspace, localized, login, uniqueName } = require('./helpers');
test('created workspace secret renders styled table actions', async ({ page }) => {
const secretName = uniqueName('ui_secret');
await login(page);
const workspace = await getCurrentWorkspace(page);
await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/secrets`,
{
name: secretName,
kind: 'token',
value: 'demo-secret-value',
},
);
await page.goto('/secrets');
const row = page.locator('#secrets-tbody tr').filter({ hasText: secretName }).first();
await expect(row).toBeVisible();
await expect(row.locator('[data-testid="secret-rotate-action"]')).toHaveClass(/btn-secondary/);
await expect(row.locator('[data-testid="secret-rotate-action"]')).toHaveClass(/table-action-btn/);
await expect(row.locator('[data-testid="secret-delete-action"]')).toHaveClass(/btn-danger/);
await expect(row.locator('[data-testid="secret-delete-action"]')).toHaveClass(/table-action-btn/);
});
test('secret modal shows fields for selected secret kind only', async ({ page }) => {
await login(page);
await page.goto('/secrets');
await page.locator('[data-testid="secret-create-button"]').click();
await expect(page.locator('[data-testid="secret-create-modal"]')).toBeVisible();
const stringField = page.locator('[data-kind-field="string"]');
const basicField = page.locator('[data-kind-field="basic"]');
const jsonField = page.locator('[data-kind-field="json"]');
const kindSelect = page.locator('[data-testid="secret-kind-select"]');
await expect(stringField).toBeVisible();
await expect(basicField).toBeHidden();
await expect(jsonField).toBeHidden();
await kindSelect.selectOption('username_password');
await expect(stringField).toBeHidden();
await expect(basicField).toBeVisible();
await expect(jsonField).toBeHidden();
await kindSelect.selectOption('header');
await expect(stringField).toBeVisible();
await expect(page.locator('#secret-string-label')).toHaveText(localized('Header value', 'Значение заголовка'));
await expect(basicField).toBeHidden();
await expect(jsonField).toBeHidden();
await kindSelect.selectOption('generic');
await expect(stringField).toBeHidden();
await expect(basicField).toBeHidden();
await expect(jsonField).toBeVisible();
await kindSelect.selectOption('token');
await expect(stringField).toBeVisible();
await expect(page.locator('#secret-string-label')).toHaveText(localized('Token value', 'Значение токена'));
await expect(basicField).toBeHidden();
await expect(jsonField).toBeHidden();
});
test('generic json secret modal stays inside compact viewport', async ({ page }) => {
await page.setViewportSize({ width: 900, height: 520 });
await login(page);
await page.goto('/secrets');
await page.locator('[data-testid="secret-create-button"]').click();
await page.locator('[data-testid="secret-kind-select"]').selectOption('generic');
const modal = page.locator('[data-testid="secret-create-modal"] .modal');
await expect(modal).toBeVisible();
const box = await modal.boundingBox();
expect(box).not.toBeNull();
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.y + box.height).toBeLessThanOrEqual(520);
await expect(page.locator('[data-testid="secret-name-input"]')).toBeVisible();
await expect(page.locator('[data-testid="secret-submit-button"]')).toBeVisible();
});
test('secret modal clears sensitive values and locks duplicate submit while saving', async ({ page }) => {
await login(page);
await page.goto('/secrets');
await page.locator('[data-testid="secret-create-button"]').click();
await page.locator('[data-testid="secret-name-input"]').fill(uniqueName('ui_secret_guard'));
await page.locator('[data-testid="secret-value-input"]').fill('ui-secret-canary-close');
await page.locator('#secret-modal-cancel-btn').click();
await page.locator('[data-testid="secret-create-button"]').click();
await expect(page.locator('[data-testid="secret-name-input"]')).toHaveValue('');
await expect(page.locator('[data-testid="secret-value-input"]')).toHaveValue('');
let releaseCreate;
const createReleased = new Promise((resolve) => {
releaseCreate = resolve;
});
let createPosts = 0;
await page.route(/\/api\/admin\/workspaces\/[^/]+\/secrets$/, async (route) => {
if (route.request().method() !== 'POST') {
await route.continue();
return;
}
createPosts += 1;
await createReleased;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'secret_ui_guard',
workspace_id: 'ws_test',
name: 'ui_secret_guard',
kind: 'token',
status: 'active',
current_version: 1,
created_at: '2026-08-15T00:00:00Z',
updated_at: '2026-08-15T00:00:00Z',
last_used_at: null,
}),
});
});
await page.locator('[data-testid="secret-name-input"]').fill(uniqueName('ui_secret_guard'));
await page.locator('[data-testid="secret-value-input"]').fill('ui-secret-canary-submit');
const submit = page.locator('[data-testid="secret-submit-button"]');
await submit.click();
await expect(submit).toBeDisabled();
await submit.click({ force: true });
expect(createPosts).toBe(1);
releaseCreate();
await expect(page.locator('[data-testid="secret-create-modal"]')).toBeHidden();
expect(createPosts).toBe(1);
});
test('stale secret rotation cannot close or report success for a newer modal context', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
const firstName = uniqueName('rotate_stale_first');
const secondName = uniqueName('rotate_stale_second');
const firstSecret = await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/secrets`,
{ name: firstName, kind: 'token', value: 'rotate-stale-first-value' },
);
await browserJson(
page,
'POST',
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/secrets`,
{ name: secondName, kind: 'token', value: 'rotate-stale-second-value' },
);
let releaseRotate;
const rotateReleased = new Promise((resolve) => {
releaseRotate = resolve;
});
let secretListRequests = 0;
await page.route(/\/api\/admin\/workspaces\/[^/]+\/secrets(?:\/[^/]+\/rotate)?$/, async (route) => {
const request = route.request();
if (request.method() === 'GET') {
secretListRequests += 1;
await route.continue();
return;
}
if (request.method() === 'POST' && /\/rotate$/.test(request.url())) {
await rotateReleased;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: firstSecret.id,
workspace_id: workspace.id,
name: firstName,
kind: 'token',
status: 'active',
current_version: 2,
created_at: '2026-08-15T00:00:00Z',
updated_at: '2026-08-15T00:00:00Z',
last_used_at: null,
}),
});
return;
}
await route.continue();
});
await page.goto('/secrets');
const firstRow = page.locator('#secrets-tbody tr').filter({ hasText: firstName }).first();
const secondRow = page.locator('#secrets-tbody tr').filter({ hasText: secondName }).first();
await expect(firstRow).toBeVisible();
await expect(secondRow).toBeVisible();
const listRequestsBeforeRelease = secretListRequests;
await firstRow.getByTestId('secret-rotate-action').click();
await page.locator('[data-testid="secret-value-input"]').fill('rotate-stale-first-new-value');
await page.locator('[data-testid="secret-submit-button"]').click();
await expect(page.locator('[data-testid="secret-submit-button"]')).toBeDisabled();
await page.locator('#secret-modal-cancel-btn').click();
await expect(page.getByTestId('secret-rotate-modal')).toBeHidden();
await secondRow.getByTestId('secret-rotate-action').click();
await expect(page.getByTestId('secret-rotate-modal')).toBeVisible();
await expect(page.locator('[data-testid="secret-name-input"]')).toHaveValue(secondName);
releaseRotate();
await page.waitForTimeout(150);
await expect(page.getByTestId('secret-rotate-modal')).toBeVisible();
await expect(page.locator('[data-testid="secret-name-input"]')).toHaveValue(secondName);
await expect(page.locator('.toast-success')).toHaveCount(0);
expect(secretListRequests).toBe(listRequestsBeforeRelease);
});