diff --git a/Makefile b/Makefile index ace6c5d..32b2250 100644 --- a/Makefile +++ b/Makefile @@ -277,10 +277,10 @@ endif # ── Admin password management ────────────────────────────────────────────────── show-admin-password: - @python3 scripts/reset_admin_password.py --show + @sudo python3 scripts/reset_admin_password.py --show reset-admin-password: - @python3 scripts/reset_admin_password.py --generate + @sudo python3 scripts/reset_admin_password.py --generate test-phase1: cd api && python3 -m pytest tests/test_network_manager.py tests/test_phase1_endpoints.py -v diff --git a/tests/e2e/ui/test_admin_wireguard.py b/tests/e2e/ui/test_admin_wireguard.py index fdf56ef..68773c2 100644 --- a/tests/e2e/ui/test_admin_wireguard.py +++ b/tests/e2e/ui/test_admin_wireguard.py @@ -2,19 +2,17 @@ Admin Peers page — WireGuard peer management UI tests. Scenarios: - 8. Create peer via UI → one-time password modal ("Peer Created — Save This Password") + 8. Create peer via UI → success toast (password modal removed — admin enters it) 9. Delete peer via UI → peer disappears from the table + 10. WireGuard page port check badge renders (Open / Blocked / Checking) Key selectors confirmed from Peers.jsx: - "Add Peer" button: button with text "Add Peer" (Plus icon + text) - - Name input: input with placeholder "mobile-phone" (no autocomplete attr; class="input") + - Name input: input with placeholder "mobile-phone" - Password input: type="password" autocomplete="new-password" - - Generate (password) button: button text "Generate" - - Submit button: button text "Add Peer" (type="submit" inside the modal form) - - Password modal heading: "Peer Created — Save This Password" - - Done button in modal: button text "Done" + - Submit button: button text "Add Peer" (type="submit" inside the form) - Delete button in peer row: button title="Remove Peer" (Trash2 icon) - - Confirmation: window.confirm() — Playwright auto-accepts dialogs unless overridden + - Confirmation: window.confirm() — Playwright auto-accepts dialogs """ import pytest @@ -25,70 +23,46 @@ _UI_PEER_PASS = 'UITestPass123!' # --------------------------------------------------------------------------- -# Scenario 8 — Create peer, see one-time password modal +# Scenario 8 — Create peer → success toast (no password modal) # --------------------------------------------------------------------------- -def test_create_peer_shows_password_modal(admin_page, webui_base, admin_client): +def test_create_peer_shows_success_toast(admin_page, webui_base, admin_client): """ - Fill the Add Peer form in the browser and verify the one-time password - modal appears after submission. - - Cleanup: delete the peer via API in the finally block so subsequent tests - start from a clean state. + Fill the Add Peer form in the browser. After submission the one-time + password modal is gone (admin entered the password themselves); instead + a success toast containing the peer name should appear. """ page = admin_page - - # Auto-accept the window.confirm() that handleRemovePeer uses (not needed - # here but set up globally to avoid any accidental blocking). page.on('dialog', lambda d: d.accept()) page.goto(f"{webui_base}/peers") page.wait_for_load_state('networkidle') - # Click "Add Peer" — confirmed text from Peers.jsx line 431 add_btn = page.get_by_role('button', name='Add Peer') if not add_btn.is_visible(): pytest.skip("'Add Peer' button not visible — is the backend reachable?") add_btn.click() - - # Wait for the modal to appear (h3 "Add New Peer") page.wait_for_selector('h3:has-text("Add New Peer")', timeout=5000) - # Fill peer name — placeholder="mobile-phone" from Peers.jsx line 525 - name_input = page.locator('input[placeholder="mobile-phone"]') - name_input.fill(_UI_PEER_NAME) - - # Fill password — type=password autocomplete=new-password from Peers.jsx line 547-549 - pw_input = page.locator('input[type="password"][autocomplete="new-password"]') - pw_input.fill(_UI_PEER_PASS) + page.locator('input[placeholder="mobile-phone"]').fill(_UI_PEER_NAME) + page.locator('input[type="password"][autocomplete="new-password"]').fill(_UI_PEER_PASS) try: - # Submit — button text "Add Peer" inside the form page.get_by_role('button', name='Add Peer').last.click() - # Peers.jsx sets showPasswordModal after successful creation; heading confirmed - # at line 769: "Peer Created — Save This Password" - page.wait_for_selector( - 'h3:has-text("Peer Created")', - timeout=15000, + # Password modal must NOT appear + page.wait_for_timeout(2000) + assert not page.locator('h3:has-text("Peer Created")').is_visible(), ( + "Password modal should be gone — admin knows the password they set" ) - # The password itself should be visible in the modal - assert page.locator(f'code:has-text("{_UI_PEER_PASS}")').is_visible() - - # Close the modal - page.get_by_role('button', name='Done').click() - - # Modal should be gone - assert not page.locator('h3:has-text("Peer Created")').is_visible() + # Success toast should mention the peer name + page.wait_for_selector(f'text="{_UI_PEER_NAME}"', timeout=10000) except Exception as exc: - pytest.xfail( - f"Peer creation modal test requires selector tuning: {exc}" - ) + pytest.xfail(f"Peer creation toast test: {exc}") finally: - # Best-effort cleanup: remove via API regardless of test outcome admin_client.delete(f'/api/peers/{_UI_PEER_NAME}') @@ -142,3 +116,39 @@ def test_delete_peer_removes_from_table(admin_page, webui_base, admin_client, ma ) except Exception as exc: pytest.xfail(f"Delete peer UI test requires selector tuning: {exc}") + + +# --------------------------------------------------------------------------- +# Scenario 10 — WireGuard page port check badge renders +# --------------------------------------------------------------------------- + +def test_wireguard_port_check_badge_renders(admin_page, webui_base): + """ + Navigate to the WireGuard page (/wireguard). The server config card must + render and the port-status badge must show one of: + Open | Blocked | Checking… | Click Refresh IP to check + The badge is a driven by serverConfig.port_open in WireGuard.jsx. + The fix for this (credentials: 'include' on raw fetch calls) means the + /api/wireguard/check-port call now carries the session cookie. + """ + page = admin_page + page.goto(f"{webui_base}/wireguard") + page.wait_for_load_state('networkidle') + + try: + # Wait for the server config section to appear + page.wait_for_selector('text=Server Configuration', timeout=10000) + + # Port badge — any of the four possible states is acceptable + badge = page.locator('span', has_text='Open').or_( + page.locator('span', has_text='Blocked') + ).or_( + page.locator('span', has_text='Checking') + ).or_( + page.locator('span', has_text='Click Refresh IP') + ).first + badge.wait_for(timeout=15000) + assert badge.is_visible(), "Port status badge not visible on WireGuard page" + + except Exception as exc: + pytest.xfail(f"WireGuard port check badge test: {exc}") diff --git a/webui/src/pages/Peers.jsx b/webui/src/pages/Peers.jsx index f9fbe35..1421dee 100644 --- a/webui/src/pages/Peers.jsx +++ b/webui/src/pages/Peers.jsx @@ -67,7 +67,6 @@ function Peers() { const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); - const [showPasswordModal, setShowPasswordModal] = useState(null); const [selectedPeer, setSelectedPeer] = useState(null); const [formData, setFormData] = useState(emptyForm()); const [showAdvanced, setShowAdvanced] = useState(false); @@ -90,7 +89,7 @@ function Peers() { const [regResp, statusResp, scResp] = await Promise.all([ peerRegistryAPI.getPeers(), wireguardAPI.getPeerStatuses().catch(() => ({ data: {} })), - fetch('/api/wireguard/server-config').then(r => r.ok ? r.json() : null).catch(() => null), + fetch('/api/wireguard/server-config', { credentials: 'include' }).then(r => r.ok ? r.json() : null).catch(() => null), ]); const regPeers = regResp.data || []; const statusMap = statusResp.data || {}; @@ -115,7 +114,7 @@ function Peers() { const getServerConfig = async () => { if (serverConf) return serverConf; try { - const r = await fetch('/api/wireguard/server-config'); + const r = await fetch('/api/wireguard/server-config', { credentials: 'include' }); if (r.ok) { const sc = await r.json(); setServerConf(sc); @@ -202,18 +201,23 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; if (formData.create_calendar) { try { - await fetch(`/api/calendar/create-user-collection?user=${formData.name}`, { method: 'POST' }); + await fetch(`/api/calendar/create-user-collection?user=${formData.name}`, { method: 'POST', credentials: 'include' }); } catch {} } const provisioned = addResult.data?.provisioned; const createdName = formData.name; - const createdPassword = formData.password; + const provisionedList = provisioned + ? Object.entries(provisioned).filter(([, v]) => v).map(([k]) => k).join(', ') + : ''; setShowAddModal(false); setFormData(emptyForm()); setErrors({}); fetchPeers(); - setShowPasswordModal({ name: createdName, password: createdPassword, provisioned }); + showToast( + `Peer "${createdName}" created.` + (provisionedList ? ` Accounts: ${provisionedList}.` : ''), + 'success' + ); } catch (err) { showToast(err?.response?.data?.error || 'Failed to add peer', 'error'); } finally { setIsSubmitting(false); } @@ -227,6 +231,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; try { const r = await fetch(`/api/peers/${selectedPeer.name}`, { method: 'PUT', + credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: formData.description, @@ -294,7 +299,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; const handleConfigDownloaded = async (peerName) => { try { - await fetch(`/api/peers/${peerName}/clear-reinstall`, { method: 'POST' }); + await fetch(`/api/peers/${peerName}/clear-reinstall`, { method: 'POST', credentials: 'include' }); setPeers(ps => ps.map(p => p.name === peerName ? { ...p, config_needs_reinstall: false } : p)); } catch {} }; @@ -759,42 +764,6 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; )} - {/* One-time password modal */} - {showPasswordModal && ( -
-
-
- -

Peer Created — Save This Password

-
-

- This is the only time you will see this password. Copy it and share it with {showPasswordModal.name}. -

-
-
- {showPasswordModal.password} - -
-
- {showPasswordModal.provisioned && ( -

- Accounts created: {Object.entries(showPasswordModal.provisioned).filter(([, v]) => v).map(([k]) => k).join(', ') || 'none'} -

- )} -
- -
-
-
- )} ); } diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx index 388a97f..8fddc19 100644 --- a/webui/src/pages/WireGuard.jsx +++ b/webui/src/pages/WireGuard.jsx @@ -29,11 +29,11 @@ function WireGuard() { setIsRefreshingIp(true); try { // Refresh IP first (fast) - const ipResp = await fetch('/api/wireguard/refresh-ip', { method: 'POST' }); + const ipResp = await fetch('/api/wireguard/refresh-ip', { method: 'POST', credentials: 'include' }); const ipData = await ipResp.json(); setServerConfig(prev => ({ ...prev, ...ipData, port_open: 'checking' })); // Then check port (slow — external call) - const portResp = await fetch('/api/wireguard/check-port', { method: 'POST' }); + const portResp = await fetch('/api/wireguard/check-port', { method: 'POST', credentials: 'include' }); const portData = await portResp.json(); setServerConfig(prev => ({ ...prev, port_open: portData.port_open })); } catch (e) { @@ -49,14 +49,14 @@ function WireGuard() { wireguardAPI.getStatus(), peerAPI.getPeers(), wireguardAPI.getPeers(), - fetch('/api/wireguard/server-config').then(r => r.json()).catch(() => null), + fetch('/api/wireguard/server-config', { credentials: 'include' }).then(r => r.json()).catch(() => null), ]); setStatus(statusResponse.data); if (serverConfigResponse) { setServerConfig({ ...serverConfigResponse, port_open: 'checking' }); // Check port asynchronously so page loads fast - fetch('/api/wireguard/check-port', { method: 'POST' }) + fetch('/api/wireguard/check-port', { method: 'POST', credentials: 'include' }) .then(r => r.json()) .then(d => setServerConfig(prev => ({ ...prev, port_open: d.port_open ?? false }))) .catch(() => setServerConfig(prev => ({ ...prev, port_open: false }))); @@ -90,7 +90,7 @@ function WireGuard() { // Load all peer statuses in one call (keyed by public_key) let liveStatuses = {}; try { - const stResp = await fetch('/api/wireguard/peers/statuses'); + const stResp = await fetch('/api/wireguard/peers/statuses', { credentials: 'include' }); if (stResp.ok) liveStatuses = await stResp.json(); } catch (_) {} @@ -179,7 +179,7 @@ function WireGuard() { const getServerConfig = async () => { if (serverConfig?.public_key) return serverConfig; try { - const response = await fetch('/api/wireguard/server-config'); + const response = await fetch('/api/wireguard/server-config', { credentials: 'include' }); if (response.ok) { const config = await response.json(); setServerConfig(config); @@ -243,14 +243,11 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; const getPeerStatus = async (peer) => { try { // Get real peer status from the API - const response = await fetch('http://localhost:3000/api/wireguard/peers/status', { + const response = await fetch('/api/wireguard/peers/status', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - public_key: peer.public_key - }) + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ public_key: peer.public_key }), }); if (response.ok) {