218 lines
8.5 KiB
JavaScript
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);
|
|
});
|