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); });