fix(frontend): stabilize CrowdSec first-enable UX and guard empty-value regression
When CrowdSec is first enabled, the 10-60 second startup window caused the toggle to immediately flicker back to unchecked, the card badge to show 'Disabled' throughout startup, CrowdSecKeyWarning to flash before bouncer registration completed, and CrowdSecConfig to show alarming LAPI-not-ready banners to the user. Root cause: the toggle, badge, and warning conditions all read from stale sources (crowdsecStatus local state and status.crowdsec.enabled server data) which neither reflects user intent during a pending mutation. - Derive crowdsecChecked from crowdsecPowerMutation.variables during the pending window so the UI reflects intent immediately on click, not the lagging server state - Show a 'Starting...' badge in warning variant throughout the startup window so the user knows the operation is in progress - Suppress CrowdSecKeyWarning unconditionally while the mutation is pending, preventing the bouncer key alert from flashing before registration completes on the backend - Broadcast the mutation's running state to the QueryClient cache via a synthetic crowdsec-starting key so CrowdSecConfig.tsx can read it without prop drilling - In CrowdSecConfig, suppress the LAPI 'not running' (red) and 'initializing' (yellow) banners while the startup broadcast is active, with a 90-second safety cap to prevent stale state from persisting if the tab is closed mid-mutation - Add security.crowdsec.starting translation key to all five locales - Add two backend regression tests confirming that empty-string setting values are accepted (not rejected by binding validation), preventing silent re-introduction of the Issue 4 bug - Add nine RTL tests covering toggle stabilization, badge text, warning suppression, and LAPI banner suppression/expiry - Add four Playwright E2E tests using route interception to simulate the startup delay in a real browser context Fixes Issues 3 and 4 from the fresh-install bug report.
This commit is contained in:
98
tests/security/crowdsec-first-enable.spec.ts
Normal file
98
tests/security/crowdsec-first-enable.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* CrowdSec First-Enable UX E2E Tests
|
||||
*
|
||||
* Tests the UI behavior while the CrowdSec startup mutation is pending.
|
||||
* Uses route interception to simulate the slow startup without a real CrowdSec install.
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md PR-4
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('CrowdSec first-enable UX @security', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test('CrowdSec toggle stays checked while starting', async ({ page }) => {
|
||||
// Intercept start endpoint and hold the response for 2 seconds
|
||||
await page.route('**/api/v1/admin/crowdsec/start', async (route) => {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ pid: 123, lapi_ready: false }),
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = page.getByTestId('toggle-crowdsec');
|
||||
await toggle.click();
|
||||
|
||||
// Immediately after click, the toggle should remain checked (user intent)
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
test('CrowdSec card shows Starting badge while starting', async ({ page }) => {
|
||||
await page.route('**/api/v1/admin/crowdsec/start', async (route) => {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ pid: 123, lapi_ready: false }),
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = page.getByTestId('toggle-crowdsec');
|
||||
await toggle.click();
|
||||
|
||||
// Badge should show "Starting..." text while mutation is pending
|
||||
await expect(page.getByText('Starting...')).toBeVisible();
|
||||
});
|
||||
|
||||
test('CrowdSecKeyWarning absent while starting', async ({ page }) => {
|
||||
await page.route('**/api/v1/admin/crowdsec/start', async (route) => {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ pid: 123, lapi_ready: false }),
|
||||
});
|
||||
});
|
||||
|
||||
// Make key-status return a rejected key
|
||||
await page.route('**/api/v1/admin/crowdsec/key-status', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
env_key_rejected: true,
|
||||
key_source: 'env',
|
||||
full_key: 'key123',
|
||||
current_key_preview: 'key...',
|
||||
rejected_key_preview: 'old...',
|
||||
message: 'Key rejected',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = page.getByTestId('toggle-crowdsec');
|
||||
await toggle.click();
|
||||
|
||||
// The key warning alert must not be present while mutation is pending
|
||||
await expect(page.getByRole('alert', { name: /CrowdSec API Key/i })).not.toBeVisible({ timeout: 1500 });
|
||||
const keyWarning = page.locator('[role="alert"]').filter({ hasText: /CrowdSec API Key Updated/ });
|
||||
await expect(keyWarning).not.toBeVisible({ timeout: 500 });
|
||||
});
|
||||
|
||||
test('Backend accepts empty value for setting', async ({ page }) => {
|
||||
// Confirm POST /settings with empty value returns 200 (not 400)
|
||||
const response = await page.request.post('/api/v1/settings', {
|
||||
data: { key: 'security.crowdsec.enabled', value: '' },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user