diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index fee5dfe2..298913d3 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,74 +1,503 @@ -# Test Coverage Plan - 85%+ Coverage Target +# DNS Routes Implementation Plan -**Last Updated:** January 7, 2026 - -This document outlines the comprehensive plan to achieve 85%+ test coverage, addressing 457 lines of missing coverage across 10 files. +**Last Updated:** January 8, 2026 +**Status:** 🟡 READY FOR IMPLEMENTATION +**Priority:** P1 - Navigation broken --- -## Section 0: ACTIVE - Test Failure Remediation Plan - PR #460 + #461 + CI Run #20773147447 +## Problem Statement -**Status:** 🔴 CRITICAL - 30 Tests Failing in CI -**Created:** January 7, 2026 -**Updated:** January 7, 2026 (Added PR #460 + CI failures) -**Priority:** P0 (Blocking PR #461) -**Context:** DNS Challenge Multi-Credential Support + Additional Test Failures +The `Layout.tsx` navigation was updated to include a DNS menu with children: +- `/dns` (parent) +- `/dns/providers` +- `/dns/plugins` -### Executive Summary +However, no corresponding routes were added to `App.tsx`, causing React Router errors: +``` +No routes matched location "/dns/providers" +No routes matched location "/dns/plugins" +No routes matched location "/dns/" +``` -Comprehensive remediation plan covering test failures from multiple sources: -- **PR #461** (24 tests): DNS Challenge multi-credential support -- **PR #460 Test Output** (5 tests): Backend handler failures -- **CI Run #20773147447** (1 test): Frontend timeout +--- -**Total:** 30 unique test failures categorized into 5 root causes: -1. **DNS Provider Registry Not Initialized** - Critical blocker for 18 tests -2. **Credential Field Name Mismatches** - Affects 4 service tests -3. **Security Handler Error Handling** - 1 test returning 500 instead of 400 -4. **Security Settings Database Override** - 5 tests failing due to record not found -5. **Certificate Deletion Race Condition** - 1 test with database locking -6. **Frontend Test Timeout** - 1 test with race condition +## Current State Analysis -### Root Causes Identified +### Layout.tsx Navigation (Lines 64-69) +```tsx +{ name: t('navigation.dns'), path: '/dns', icon: '☁️', children: [ + { name: t('navigation.dnsProviders'), path: '/dns/providers', icon: '🧭' }, + { name: t('navigation.plugins'), path: '/dns/plugins', icon: '🔌' }, +] }, +``` -#### 1. DNS Provider Registry Not Initialized (CRITICAL) -- **Impact:** 18 tests failing (credential handlers + Caddy tests) -- **Issue:** `dnsprovider.Global().Get()` returns not found -- **Fix:** Create test helper to initialize registry with all providers +### App.tsx Current Routes (Lines 64-66) +The DNS-related route currently exists as: +```tsx +} /> +``` -#### 2. Credential Field Name Inconsistencies -- **Impact:** 4 DNS provider service tests -- **Providers Affected:** hetzner (api_key→api_token), digitalocean (auth_token→api_token), dnsimple (oauth_token→api_token) -- **Fix:** Update test data field names +### Existing Page Components +| Component | Path | Status | +|-----------|------|--------| +| `DNSProviders.tsx` | `/pages/DNSProviders.tsx` | ✅ Exists | +| `Plugins.tsx` | `/pages/Plugins.tsx` | ✅ Exists | -#### 3. Security Handler Returns 500 for Invalid Input -- **Impact:** 1 security audit test -- **Issue:** Missing input validation before database operations -- **Fix:** Add validation returning 400 for malicious inputs +### Pattern Reference: Nested Routes -#### 4. Security Settings Database Override Not Working (NEW - PR #460) -- **Impact:** 5 security handler tests -- **Tests Failing:** - - `TestSecurityHandler_ACL_DBOverride` - - `TestSecurityHandler_CrowdSec_Mode_DBOverride` - - `TestSecurityHandler_GetStatus_RespectsSettingsTable` (5 sub-tests) - - `TestSecurityHandler_GetStatus_WAFModeFromSettings` - - `TestSecurityHandler_GetStatus_RateLimitModeFromSettings` -- **Issue:** `GetStatus` handler returns "record not found" when querying `security_configs` table -- **Root Cause:** Handler queries `security_configs` table which is empty in tests, but tests expect settings from `settings` table to override config -- **Fix:** Ensure handler checks `settings` table first before falling back to `security_configs` +**Settings Pattern** (Lines 79-87 in App.tsx): +```tsx +}> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +``` -#### 5. Certificate Deletion Database Lock (NEW - PR #460) -- **Impact:** 1 test (`TestDeleteCertificate_CreatesBackup`) -- **Issue:** `database table is locked: ssl_certificates` causing 500 error -- **Root Cause:** SQLite in-memory database lock contention when backup and delete happen in rapid succession -- **Fix:** Add proper transaction handling or retry logic with backoff +**Settings.tsx Structure**: +- Uses `` to render child routes +- Provides tab navigation UI for child pages +- Wraps content in `PageShell` component -#### 6. Frontend LiveLogViewer Test Timeout (NEW - CI #20773147447) -- **Impact:** 1 test (`LiveLogViewer.test.tsx:374` - "displays blocked requests with special styling") -- **Issue:** Test times out after 5000ms waiting for DOM elements -- **Root Cause:** Multiple assertions in single `waitFor` block + complex regex matching + async state update timing -- **Fix:** Split assertions into separate `waitFor` calls or use `findBy` queries +**Tasks Pattern** (Lines 89-98 in App.tsx): +```tsx +}> + } /> + } /> + } /> + + } /> + } /> + + +``` + +--- + +## Implementation Plan + +### Phase 1: Create DNS Parent Page + +**File**: `frontend/src/pages/DNS.tsx` (NEW) + +```tsx +import { Link, Outlet, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { PageShell } from '../components/layout/PageShell' +import { cn } from '../utils/cn' +import { Cloud, Puzzle } from 'lucide-react' + +export default function DNS() { + const { t } = useTranslation() + const location = useLocation() + + const isActive = (path: string) => location.pathname === path + + const navItems = [ + { path: '/dns/providers', label: t('navigation.dnsProviders'), icon: Cloud }, + { path: '/dns/plugins', label: t('navigation.plugins'), icon: Puzzle }, + ] + + return ( + + + + } + > + {/* Tab Navigation */} + + + {/* Content Area */} +
+ +
+
+ ) +} +``` + +--- + +### Phase 2: Update App.tsx Routes + +**File**: `frontend/src/App.tsx` + +#### Changes Required: + +1. **Add lazy import** (after line 24): +```tsx +const DNS = lazy(() => import('./pages/DNS')) +``` + +2. **Replace single dns-providers route** (line 66): + +**REMOVE**: +```tsx +} /> +``` + +**ADD** (after the domains route, ~line 65): +```tsx +{/* DNS Routes */} +}> + } /> + } /> + } /> + + +{/* Legacy redirect for old bookmarks */} +} /> +``` + +#### Full Diff Preview: + +```diff + const Dashboard = lazy(() => import('./pages/Dashboard')) + const ProxyHosts = lazy(() => import('./pages/ProxyHosts')) + const RemoteServers = lazy(() => import('./pages/RemoteServers')) ++const DNS = lazy(() => import('./pages/DNS')) + const ImportCaddy = lazy(() => import('./pages/ImportCaddy')) +``` + +```diff + } /> + } /> +- } /> ++ ++ {/* DNS Routes */} ++ }> ++ } /> ++ } /> ++ } /> ++ ++ ++ {/* Legacy redirect for old bookmarks */} ++ } /> ++ + } /> +``` + +--- + +### Phase 3: Update DNSProviders.tsx + +The `DNSProviders.tsx` component currently uses `PageShell` directly. When rendered inside `DNS.tsx`, it will be wrapped twice. We need to remove `PageShell` since the parent DNS component provides it. + +**File**: `frontend/src/pages/DNSProviders.tsx` + +**Changes Required**: + +```diff +-import { PageShell } from '../components/layout/PageShell' ++// PageShell removed - parent DNS component provides the shell + + export default function DNSProviders() { + // ... existing state and handlers ... + +- return ( +- ++ return ( ++
++ {/* Header with Add Button */} ++
++ {headerActions} ++
++ + {/* Info Alert */} + + ... + + + {/* Loading State */} + ... + + {/* Empty State */} + ... + + {/* Provider Cards Grid */} + ... + + {/* Add/Edit Form Dialog */} + ... +- ++
+ ) + } +``` + +--- + +### Phase 4: Update Plugins.tsx + +**File**: `frontend/src/pages/Plugins.tsx` + +Apply the same pattern - remove `PageShell` wrapper since `DNS.tsx` provides it. + +**Changes Required**: + +```diff +-import { PageShell } from '../components/layout/PageShell' ++// PageShell removed - parent DNS component provides the shell + + export default function Plugins() { + // ... existing state and handlers ... + +- return ( +- ++ return ( ++
++ {/* Header with Reload Button */} ++
++ {headerActions} ++
++ + {/* Info Alert */} + ... + + {/* Loading State */} + ... + + {/* Empty State */} + ... + + {/* Built-in Plugins Section */} + ... + + {/* External Plugins Section */} + ... + + {/* Metadata Modal */} + ... +- ++
+ ) + } +``` + +--- + +### Phase 5: Add i18n Translation Keys + +**File**: `frontend/src/locales/en/translation.json` + +Add to the `navigation` section (around line 54): + +```json +"dns": "DNS", +``` + +Add new section for DNS parent page (after `dnsProviders` section): + +```json +"dns": { + "title": "DNS Management", + "description": "Manage DNS providers and plugins for certificate automation" +}, +``` + +**Files to update** (all locale files): +- `frontend/src/locales/en/translation.json` +- `frontend/src/locales/de/translation.json` +- `frontend/src/locales/es/translation.json` +- `frontend/src/locales/fr/translation.json` +- `frontend/src/locales/zh/translation.json` + +--- + +### Phase 6: Remove admin/plugins Route (Cleanup) + +**File**: `frontend/src/App.tsx` + +The current route at line 76: +```tsx +} /> +``` + +This should be **replaced** with a redirect for backwards compatibility: + +```tsx +} /> +``` + +--- + +## Test Updates + +### New Test File: DNS.test.tsx + +**File**: `frontend/src/pages/__tests__/DNS.test.tsx` (NEW) + +```tsx +import { describe, it, expect, vi } from 'vitest' +import { screen, within } from '@testing-library/react' +import DNS from '../DNS' +import { renderWithQueryClient } from '../../test-utils/renderWithQueryClient' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'dns.title': 'DNS Management', + 'dns.description': 'Manage DNS providers and plugins', + 'navigation.dnsProviders': 'DNS Providers', + 'navigation.plugins': 'Plugins', + } + return translations[key] || key + }, + }), +})) + +describe('DNS page', () => { + it('renders DNS management page with navigation tabs', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + expect(await screen.findByText('DNS Management')).toBeInTheDocument() + expect(screen.getByText('DNS Providers')).toBeInTheDocument() + expect(screen.getByText('Plugins')).toBeInTheDocument() + }) + + it('highlights active tab based on route', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + const nav = screen.getByRole('navigation') + const providersLink = within(nav).getByText('DNS Providers').closest('a') + + // Active tab should have the elevated style class + expect(providersLink).toHaveClass('bg-surface-elevated') + }) + + it('provides tab navigation links', async () => { + renderWithQueryClient(, { routeEntries: ['/dns'] }) + + const providersLink = screen.getByRole('link', { name: /dns providers/i }) + const pluginsLink = screen.getByRole('link', { name: /plugins/i }) + + expect(providersLink).toHaveAttribute('href', '/dns/providers') + expect(pluginsLink).toHaveAttribute('href', '/dns/plugins') + }) +}) +``` + +### Update Existing Tests + +**File**: `frontend/src/pages/__tests__/Plugins.test.tsx` + +Add route entry to test context since Plugins is now a child route: + +```diff +- renderWithQueryClient() ++ renderWithQueryClient(, { routeEntries: ['/dns/plugins'] }) +``` + +--- + +## Files Summary + +| File | Action | Description | +|------|--------|-------------| +| `frontend/src/pages/DNS.tsx` | **CREATE** | New parent component for DNS routes | +| `frontend/src/App.tsx` | **MODIFY** | Add DNS route group, legacy redirects | +| `frontend/src/pages/DNSProviders.tsx` | **MODIFY** | Remove PageShell wrapper | +| `frontend/src/pages/Plugins.tsx` | **MODIFY** | Remove PageShell wrapper | +| `frontend/src/locales/en/translation.json` | **MODIFY** | Add DNS translation keys | +| `frontend/src/locales/de/translation.json` | **MODIFY** | Add DNS translation keys | +| `frontend/src/locales/es/translation.json` | **MODIFY** | Add DNS translation keys | +| `frontend/src/locales/fr/translation.json` | **MODIFY** | Add DNS translation keys | +| `frontend/src/locales/zh/translation.json` | **MODIFY** | Add DNS translation keys | +| `frontend/src/pages/__tests__/DNS.test.tsx` | **CREATE** | Tests for DNS parent component | +| `frontend/src/pages/__tests__/Plugins.test.tsx` | **MODIFY** | Update route entries | + +--- + +## Verification Checklist + +- [ ] `/dns` route renders DNS page with providers as default +- [ ] `/dns/providers` renders DNSProviders component +- [ ] `/dns/plugins` renders Plugins component +- [ ] `/dns-providers` redirects to `/dns/providers` +- [ ] `/admin/plugins` redirects to `/dns/plugins` +- [ ] Navigation sidebar correctly highlights active routes +- [ ] Tab navigation within DNS page works correctly +- [ ] All existing DNS provider functionality preserved +- [ ] All existing Plugins functionality preserved +- [ ] Tests pass: `npm test` +- [ ] TypeScript compiles: `npm run type-check` +- [ ] Linting passes: `npm run lint` + +--- + +## Implementation Order + +1. Create `DNS.tsx` parent component +2. Update `App.tsx` with new route structure +3. Modify `DNSProviders.tsx` to remove PageShell +4. Modify `Plugins.tsx` to remove PageShell +5. Add i18n translation keys (all locales) +6. Create `DNS.test.tsx` +7. Update `Plugins.test.tsx` route entries +8. Run tests and verify +9. Manual browser testing + +--- + +## Risk Assessment + +| Risk | Mitigation | +|------|-----------| +| Breaking existing `/dns-providers` bookmarks | Add redirect route | +| Breaking `/admin/plugins` URL | Add redirect route | +| Double PageShell wrapping | Remove PageShell from child components | +| Missing translations | Add keys to all 5 locale files | +| Test failures | Update route entries in test renders | + +--- + +## Estimated Effort + +| Task | Time | +|------|------| +| Create DNS.tsx | 15 min | +| Update App.tsx routes | 10 min | +| Update DNSProviders.tsx | 10 min | +| Update Plugins.tsx | 10 min | +| Update i18n files (5) | 15 min | +| Create DNS.test.tsx | 15 min | +| Update Plugins.test.tsx | 5 min | +| Testing & verification | 20 min | +| **Total** | **~1.5 hours** ### Remediation Phases diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ca645cfd..5ff61693 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import { AuthProvider } from './context/AuthContext' const Dashboard = lazy(() => import('./pages/Dashboard')) const ProxyHosts = lazy(() => import('./pages/ProxyHosts')) const RemoteServers = lazy(() => import('./pages/RemoteServers')) +const DNS = lazy(() => import('./pages/DNS')) const ImportCaddy = lazy(() => import('./pages/ImportCaddy')) const ImportCrowdSec = lazy(() => import('./pages/ImportCrowdSec')) const Certificates = lazy(() => import('./pages/Certificates')) @@ -63,7 +64,17 @@ export default function App() { } /> } /> } /> - } /> + + {/* DNS Routes */} + }> + } /> + } /> + } /> + + + {/* Legacy redirect for old bookmarks */} + } /> + } /> } /> } /> @@ -75,7 +86,7 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> {/* Settings Routes */} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 54924b16..25c96547 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -63,7 +63,10 @@ export default function Layout({ children }: LayoutProps) { { name: t('navigation.remoteServers'), path: '/remote-servers', icon: '🖥️' }, { name: t('navigation.domains'), path: '/domains', icon: '🌍' }, { name: t('navigation.certificates'), path: '/certificates', icon: '🔒' }, - { name: t('navigation.dnsProviders'), path: '/dns-providers', icon: '☁️' }, + { name: t('navigation.dns'), path: '/dns', icon: '☁️', children: [ + { name: t('navigation.dnsProviders'), path: '/dns/providers', icon: '🧭' }, + { name: t('navigation.plugins'), path: '/dns/plugins', icon: '🔌' }, + ] }, { name: t('navigation.uptime'), path: '/uptime', icon: '📈' }, { name: t('navigation.security'), path: '/security', icon: '🛡️', children: [ { name: t('navigation.dashboard'), path: '/security', icon: '🛡️' }, @@ -86,14 +89,6 @@ export default function Layout({ children }: LayoutProps) { { name: t('navigation.accountManagement'), path: '/settings/account-management', icon: '👥' }, ] }, - { - name: t('navigation.admin'), - path: '/admin', - icon: '👑', - children: [ - { name: t('navigation.plugins'), path: '/admin/plugins', icon: '🔌' }, - ] - }, { name: t('navigation.tasks'), path: '/tasks', diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json index 4dcfa1b0..1b4de0cd 100644 --- a/frontend/src/locales/de/translation.json +++ b/frontend/src/locales/de/translation.json @@ -51,6 +51,9 @@ "remoteServers": "Remote-Server", "domains": "Domänen", "certificates": "Zertifikate", + "dns": "DNS", + "dnsProviders": "DNS-Anbieter", + "plugins": "Plugins", "security": "Sicherheit", "accessLists": "Zugriffslisten", "crowdsec": "CrowdSec", @@ -974,5 +977,9 @@ "strict": "Starke Sicherheit für Web-Anwendungen.\n✓ Ideal für: Web-only Dashboards, Admin-Panels.\n⚠ Kann mobile Apps und API-Clients beeinträchtigen.\nNicht empfohlen für Radarr, Plex oder Dienste mit Companion-Apps.", "paranoid": "Maximale Sicherheit für Hochrisiko-Anwendungen.\n✓ Ideal für: Banking, Gesundheitswesen, Compliance-kritische Apps.\n⚠ WIRD mobile Apps, API-Clients und OAuth-Flows beeinträchtigen.\nNur verwenden, wenn Sie jeden Header verstehen und anpassen können." } + }, + "dns": { + "title": "DNS-Verwaltung", + "description": "DNS-Anbieter und Plugins für die Zertifikatsautomatisierung verwalten" } } diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 262b6123..2ecac978 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -51,6 +51,7 @@ "remoteServers": "Remote Servers", "domains": "Domains", "certificates": "Certificates", + "dns": "DNS", "dnsProviders": "DNS Providers", "security": "Security", "accessLists": "Access Lists", @@ -1027,6 +1028,10 @@ "paranoid": "Maximum security for high-risk applications.\n✓ Best for: Banking, healthcare, compliance-critical apps.\n⚠ WILL break mobile apps, API clients, and OAuth flows.\nOnly use if you understand and can customize every header." } }, + "dns": { + "title": "DNS Management", + "description": "Manage DNS providers and plugins for certificate automation" + }, "dnsProviders": { "title": "DNS Providers", "description": "Manage DNS providers for wildcard certificate validation", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 06c16e4c..eb540dae 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -51,6 +51,9 @@ "remoteServers": "Servidores Remotos", "domains": "Dominios", "certificates": "Certificados", + "dns": "DNS", + "dnsProviders": "Proveedores DNS", + "plugins": "Plugins", "security": "Seguridad", "accessLists": "Listas de Acceso", "crowdsec": "CrowdSec", @@ -974,5 +977,9 @@ "strict": "Seguridad fuerte para aplicaciones web.\n✓ Ideal para: Dashboards solo web, paneles de administración.\n⚠ Puede afectar apps móviles y clientes API.\nNo recomendado para Radarr, Plex o servicios con apps companion.", "paranoid": "Seguridad máxima para aplicaciones de alto riesgo.\n✓ Ideal para: Banca, salud, apps críticas de cumplimiento.\n⚠ AFECTARÁ apps móviles, clientes API y flujos OAuth.\nSolo úselo si entiende y puede personalizar cada cabecera." } + }, + "dns": { + "title": "Gestión DNS", + "description": "Administrar proveedores DNS y plugins para la automatización de certificados" } } diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json index aa553aeb..e8b64cf2 100644 --- a/frontend/src/locales/fr/translation.json +++ b/frontend/src/locales/fr/translation.json @@ -51,6 +51,9 @@ "remoteServers": "Serveurs Distants", "domains": "Domaines", "certificates": "Certificats", + "dns": "DNS", + "dnsProviders": "Fournisseurs DNS", + "plugins": "Plugins", "security": "Sécurité", "accessLists": "Listes d'Accès", "crowdsec": "CrowdSec", @@ -974,5 +977,9 @@ "strict": "Sécurité renforcée pour les applications web.\n✓ Idéal pour : Tableaux de bord web uniquement, panneaux d'administration.\n⚠ Peut affecter les apps mobiles et clients API.\nNon recommandé pour Radarr, Plex ou services avec apps companion.", "paranoid": "Sécurité maximale pour les applications à haut risque.\n✓ Idéal pour : Banque, santé, apps critiques de conformité.\n⚠ AFFECTERA les apps mobiles, clients API et flux OAuth.\nUtilisez uniquement si vous comprenez et pouvez personnaliser chaque en-tête." } + }, + "dns": { + "title": "Gestion DNS", + "description": "Gérer les fournisseurs DNS et les plugins pour l'automatisation des certificats" } } diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index fb262182..d56fc6aa 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -51,6 +51,9 @@ "remoteServers": "远程服务器", "domains": "域名", "certificates": "证书", + "dns": "DNS", + "dnsProviders": "DNS 提供商", + "plugins": "插件", "security": "安全", "accessLists": "访问列表", "crowdsec": "CrowdSec", @@ -976,5 +979,9 @@ "strict": "Web 应用程序的强安全性。\n✓ 适用于:纯 Web 仪表板、管理面板。\n⚠ 可能会影响移动应用和 API 客户端。\n不推荐用于 Radarr、Plex 或带有配套应用的服务。", "paranoid": "高风险应用程序的最大安全性。\n✓ 适用于:银行、医疗、合规关键应用。\n⚠ 将会影响移动应用、API 客户端和 OAuth 流程。\n仅在您了解并能自定义每个头时使用。" } + }, + "dns": { + "title": "DNS 管理", + "description": "管理 DNS 提供商和插件以实现证书自动化" } } diff --git a/frontend/src/pages/DNS.tsx b/frontend/src/pages/DNS.tsx new file mode 100644 index 00000000..36735a55 --- /dev/null +++ b/frontend/src/pages/DNS.tsx @@ -0,0 +1,53 @@ +import { Link, Outlet, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { PageShell } from '../components/layout/PageShell' +import { cn } from '../utils/cn' +import { Cloud, Puzzle } from 'lucide-react' + +export default function DNS() { + const { t } = useTranslation() + const location = useLocation() + + const isActive = (path: string) => location.pathname === path + + const navItems = [ + { path: '/dns/providers', label: t('navigation.dnsProviders'), icon: Cloud }, + { path: '/dns/plugins', label: t('navigation.plugins'), icon: Puzzle }, + ] + + return ( + + + + } + > + {/* Tab Navigation */} + + + {/* Content Area */} +
+ +
+
+ ) +} diff --git a/frontend/src/pages/DNSProviders.tsx b/frontend/src/pages/DNSProviders.tsx index f9f8501a..4e4be582 100644 --- a/frontend/src/pages/DNSProviders.tsx +++ b/frontend/src/pages/DNSProviders.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Plus, Cloud } from 'lucide-react' -import { PageShell } from '../components/layout/PageShell' import { Button, Alert, EmptyState, Skeleton } from '../components/ui' import DNSProviderCard from '../components/DNSProviderCard' import DNSProviderForm from '../components/DNSProviderForm' @@ -76,11 +75,12 @@ export default function DNSProviders() { ) return ( - +
+ {/* Header with Add Button */} +
+ {headerActions} +
+ {/* Info Alert */} {t('dnsProviders.note')}: {t('dnsProviders.noteText')} @@ -131,6 +131,6 @@ export default function DNSProviders() { provider={editingProvider} onSuccess={handleFormSuccess} /> - +
) } diff --git a/frontend/src/pages/Plugins.tsx b/frontend/src/pages/Plugins.tsx index 0c390d1e..ee9bd511 100644 --- a/frontend/src/pages/Plugins.tsx +++ b/frontend/src/pages/Plugins.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { RefreshCw, Package, AlertCircle, CheckCircle, XCircle, Info } from 'lucide-react' -import { PageShell } from '../components/layout/PageShell' import { Button, Badge, @@ -126,14 +125,12 @@ export default function Plugins() { ) return ( - +
+ {/* Header with Reload Button */} +
+ {headerActions} +
+ {/* Info Alert */} {t('plugins.note', 'Note')}:{' '} @@ -387,6 +384,6 @@ export default function Plugins() { - +
) } diff --git a/frontend/src/pages/__tests__/DNS.test.tsx b/frontend/src/pages/__tests__/DNS.test.tsx new file mode 100644 index 00000000..85484d82 --- /dev/null +++ b/frontend/src/pages/__tests__/DNS.test.tsx @@ -0,0 +1,83 @@ +import { describe, it, expect, vi } from 'vitest' +import { screen, within } from '@testing-library/react' +import DNS from '../DNS' +import { renderWithQueryClient } from '../../test-utils/renderWithQueryClient' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'dns.title': 'DNS Management', + 'dns.description': 'Manage DNS providers and plugins for certificate automation', + 'navigation.dnsProviders': 'DNS Providers', + 'navigation.plugins': 'Plugins', + } + return translations[key] || key + }, + }), +})) + +describe('DNS page', () => { + it('renders DNS management page with navigation tabs', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + expect(await screen.findByText('DNS Management')).toBeInTheDocument() + expect(screen.getByText('DNS Providers')).toBeInTheDocument() + expect(screen.getByText('Plugins')).toBeInTheDocument() + }) + + it('renders the navigation component', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + const nav = await screen.findByRole('navigation') + expect(nav).toBeInTheDocument() + }) + + it('highlights active tab based on route', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + const nav = await screen.findByRole('navigation') + const providersLink = within(nav).getByText('DNS Providers').closest('a') + + // Active tab should have the elevated style class + expect(providersLink).toHaveClass('bg-surface-elevated') + }) + + it('displays plugins tab as inactive when on providers route', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + const nav = await screen.findByRole('navigation') + const pluginsLink = within(nav).getByText('Plugins').closest('a') + + // Inactive tab should not have the elevated style class + expect(pluginsLink).not.toHaveClass('bg-surface-elevated') + expect(pluginsLink).toHaveClass('text-content-secondary') + }) + + it('renders navigation links with correct paths', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + const nav = await screen.findByRole('navigation') + const providersLink = within(nav).getByText('DNS Providers').closest('a') + const pluginsLink = within(nav).getByText('Plugins').closest('a') + + expect(providersLink).toHaveAttribute('href', '/dns/providers') + expect(pluginsLink).toHaveAttribute('href', '/dns/plugins') + }) + + it('renders content area for child routes', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + // The content area should be rendered with the border-border class + const contentArea = document.querySelector('.bg-surface-elevated.border.border-border') + expect(contentArea).toBeInTheDocument() + }) + + it('renders cloud icon in header actions', async () => { + renderWithQueryClient(, { routeEntries: ['/dns/providers'] }) + + // Look for the Cloud icon in the header actions area + const header = await screen.findByText('DNS Management') + expect(header).toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/__tests__/Plugins.test.tsx b/frontend/src/pages/__tests__/Plugins.test.tsx index ed2513eb..ddf26278 100644 --- a/frontend/src/pages/__tests__/Plugins.test.tsx +++ b/frontend/src/pages/__tests__/Plugins.test.tsx @@ -121,9 +121,10 @@ describe('Plugins page', () => { it('renders plugin management page', async () => { renderWithQueryClient() - expect(await screen.findByText('DNS Provider Plugins')).toBeInTheDocument() - // Check that page renders without errors - expect(screen.getByRole('button', { name: /reload plugins/i })).toBeInTheDocument() + // The page now renders inside DNS parent which provides the PageShell + // Check that page content renders without errors + expect(await screen.findByRole('button', { name: /reload plugins/i })).toBeInTheDocument() + expect(screen.getByText('Note:')).toBeInTheDocument() }) it('displays built-in plugins section', async () => {