Files
nginx-ui/e2e/tests/nginx-host-setup.spec.ts
0xJacky 9fa6be8195 fix(app): harden nginx host setup control mode and wizard steps
- Render the control-mode radio group only while editing, so the read-only
  view can no longer mutate the store without passing the 2FA-gated Edit flow
- Show a dedicated message when no private key exists at the given path
  instead of the raw transport error
- Share parseHostAddress between the SSH target and host key steps so
  bracketed IPv6 loopback targets no longer trigger the remote-address warning
- Keep a stale host key scan or connection test from clearing the loading
  state of a newer in-flight run
- Align the E2E spec with the read-only control mode view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:25:29 +08:00

85 lines
3.7 KiB
TypeScript

import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { gotoRoute } from './helpers'
// The public demo runs with 2FA disabled for the admin account, so every
// protected nginx control flow stops at the "Two-factor authentication
// required" guard before it can reach /api/host/* or POST /api/settings.
// These tests therefore only assert DOM state and that no mutating or
// host-setup request is ever issued.
function trackApiRequests(page: Page) {
const requests: string[] = []
page.on('request', request => {
const url = new URL(request.url())
if (url.pathname.startsWith('/api/'))
requests.push(`${request.method()} ${url.pathname}`)
})
return requests
}
function expectReadOnlyTraffic(requests: string[]) {
expect(requests.filter(entry => entry.includes('/api/host/'))).toEqual([])
expect(requests.filter(entry => !entry.startsWith('GET ') && entry.endsWith('/api/settings'))).toEqual([])
}
function controlModeItem(page: Page) {
return page.locator('.ant-form-item').filter({ has: page.getByText('Nginx Control Mode', { exact: true }) }).first()
}
test('Nginx control mode stays read-only until Edit passes the 2FA guard', async ({ page }) => {
const requests = trackApiRequests(page)
await gotoRoute(page, '/preference')
await page.getByRole('tab', { name: 'Nginx', exact: true }).click()
const item = controlModeItem(page)
await expect(item).toBeVisible()
await expect(item.locator('.ant-tag').filter({ hasText: /^Local$/ })).toBeVisible()
const editButton = item.getByRole('button', { name: /Edit/ })
await expect(editButton).toBeVisible()
// The mode radios only mount in edit mode, so the read-only view cannot
// mutate the settings store without going through the guarded Edit flow.
await expect(item.locator('.ant-radio-group')).toHaveCount(0)
await expect(item.getByRole('radio')).toHaveCount(0)
// Editing the control mode is protected by a secure session. With 2FA
// disabled on the demo account the UI guides to 2FA settings instead.
await editButton.click()
const guard = page.locator('.ant-modal').filter({ hasText: 'Two-factor authentication required' })
await expect(guard).toBeVisible()
await guard.getByRole('button', { name: 'Cancel', exact: true }).click()
await expect(guard).toBeHidden()
// The guard cancelled the edit, so the view stays read-only.
await expect(item.locator('.ant-tag').filter({ hasText: /^Local$/ })).toBeVisible()
await expect(item.getByRole('radio')).toHaveCount(0)
expectReadOnlyTraffic(requests)
})
test('Host SSH setup wizard route stops at the 2FA guard without calling api/host', async ({ page }) => {
const requests = trackApiRequests(page)
await gotoRoute(page, '/preference/nginx-host-setup')
const wizardPage = page.locator('.host-setup-page')
await expect(wizardPage.getByRole('button', { name: /Back to Nginx settings/ })).toBeVisible()
const result = wizardPage.locator('.ant-result-403')
await expect(result).toBeVisible()
await expect(result).toContainText('Two-factor authentication required')
await expect(result).toContainText('Enable two-factor authentication before changing Nginx control settings.')
await expect(result.getByRole('button', { name: '2FA Settings', exact: true })).toBeVisible()
// The stepper only mounts once a secure session is established.
await expect(wizardPage.locator('.ant-steps')).toHaveCount(0)
await result.getByRole('button', { name: 'Back', exact: true }).click()
await expect.poll(() => new URL(page.url()).hash).toBe('#/preference?tab=nginx')
await expect(page.getByRole('tab', { name: 'Nginx', exact: true })).toHaveAttribute('aria-selected', 'true')
expectReadOnlyTraffic(requests)
})