fix: update SSL card logic to correctly detect pending certificates by domain matching
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import type { Certificate } from '../api/certificates'
|
||||
@@ -13,14 +14,46 @@ export default function CertificateStatusCard({ certificates, hosts }: Certifica
|
||||
const expiringCount = certificates.filter(c => c.status === 'expiring').length
|
||||
const untrustedCount = certificates.filter(c => c.status === 'untrusted').length
|
||||
|
||||
// Pending = hosts with ssl_forced and enabled but no certificate_id
|
||||
const sslHosts = hosts.filter(h => h.ssl_forced && h.enabled)
|
||||
const hostsWithCerts = sslHosts.filter(h => h.certificate_id != null)
|
||||
const pendingCount = sslHosts.length - hostsWithCerts.length
|
||||
// Build a set of all domains that have certificates (case-insensitive)
|
||||
// ACME certificates (Let's Encrypt) are auto-managed and don't set certificate_id,
|
||||
// so we match by domain name instead
|
||||
const certifiedDomains = useMemo(() => {
|
||||
const domains = new Set<string>()
|
||||
certificates.forEach(cert => {
|
||||
// Handle missing or undefined domain field
|
||||
if (!cert.domain) return
|
||||
// Certificate domain field can be comma-separated
|
||||
cert.domain.split(',').forEach(d => {
|
||||
const trimmed = d.trim().toLowerCase()
|
||||
if (trimmed) domains.add(trimmed)
|
||||
})
|
||||
})
|
||||
return domains
|
||||
}, [certificates])
|
||||
|
||||
// Calculate pending hosts: SSL-enabled hosts without any domain covered by a certificate
|
||||
const { pendingCount, totalSSLHosts, hostsWithCerts } = useMemo(() => {
|
||||
const sslHosts = hosts.filter(h => h.ssl_forced && h.enabled)
|
||||
|
||||
let withCerts = 0
|
||||
sslHosts.forEach(host => {
|
||||
// Check if any of the host's domains have a certificate
|
||||
const hostDomains = host.domain_names.split(',').map(d => d.trim().toLowerCase())
|
||||
if (hostDomains.some(domain => certifiedDomains.has(domain))) {
|
||||
withCerts++
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pendingCount: sslHosts.length - withCerts,
|
||||
totalSSLHosts: sslHosts.length,
|
||||
hostsWithCerts: withCerts,
|
||||
}
|
||||
}, [hosts, certifiedDomains])
|
||||
|
||||
const hasProvisioning = pendingCount > 0
|
||||
const progressPercent = sslHosts.length > 0
|
||||
? Math.round((hostsWithCerts.length / sslHosts.length) * 100)
|
||||
const progressPercent = totalSSLHosts > 0
|
||||
? Math.round((hostsWithCerts / totalSSLHosts) * 100)
|
||||
: 100
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,6 +37,19 @@ const mockHost: ProxyHost = {
|
||||
locations: [],
|
||||
}
|
||||
|
||||
// Helper to create a certificate with a specific domain
|
||||
function mockCertWithDomain(domain: string, status: 'valid' | 'expiring' | 'expired' | 'untrusted' = 'valid'): Certificate {
|
||||
return {
|
||||
id: Math.floor(Math.random() * 10000),
|
||||
name: domain,
|
||||
domain: domain,
|
||||
issuer: "Let's Encrypt",
|
||||
expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status,
|
||||
provider: 'letsencrypt',
|
||||
}
|
||||
}
|
||||
|
||||
function renderWithRouter(ui: React.ReactNode) {
|
||||
return render(<BrowserRouter>{ui}</BrowserRouter>)
|
||||
}
|
||||
@@ -95,72 +108,9 @@ describe('CertificateStatusCard', () => {
|
||||
expect(screen.queryByText(/staging/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows pending indicator when hosts lack certificates', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
expect(screen.getByText('1 host awaiting certificate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows plural for multiple pending hosts', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h3', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
expect(screen.getByText('3 hosts awaiting certificate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides pending indicator when all hosts have certificates', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('ignores disabled hosts when calculating pending', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', ssl_forced: true, certificate_id: null, enabled: false },
|
||||
{ ...mockHost, uuid: 'h2', ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('ignores hosts without SSL when calculating pending', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', ssl_forced: false, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calculates progress percentage correctly', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h3', ssl_forced: true, certificate_id: 1, enabled: true },
|
||||
{ ...mockHost, uuid: 'h4', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />)
|
||||
|
||||
// 2 out of 4 = 50%
|
||||
expect(screen.getByText('50% provisioned')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows spinning loader icon when pending', () => {
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, domain_names: 'other.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
const { container } = renderWithRouter(
|
||||
<CertificateStatusCard certificates={[mockCert]} hosts={hosts} />
|
||||
@@ -184,3 +134,188 @@ describe('CertificateStatusCard', () => {
|
||||
expect(screen.getByText('0 valid')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CertificateStatusCard - Domain Matching', () => {
|
||||
it('does not show pending when host domain matches certificate domain', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('example.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
// Should NOT show "awaiting certificate" since domain matches
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows pending when host domain has no matching certificate', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('other.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.getByText('1 host awaiting certificate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows plural for multiple pending hosts', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('has-cert.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', domain_names: 'no-cert-1.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', domain_names: 'no-cert-2.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h3', domain_names: 'no-cert-3.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.getByText('3 hosts awaiting certificate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles case-insensitive domain matching', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('EXAMPLE.COM')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles case-insensitive matching with host uppercase', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('example.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'EXAMPLE.COM', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles multi-domain hosts with partial certificate coverage', () => {
|
||||
// Host has two domains, but only one has a certificate - should be "covered"
|
||||
const certs: Certificate[] = [mockCertWithDomain('example.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com, www.example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
// Host should be considered "covered" if any domain has a cert
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles comma-separated certificate domains', () => {
|
||||
const certs: Certificate[] = [{
|
||||
...mockCertWithDomain('example.com'),
|
||||
domain: 'example.com, www.example.com'
|
||||
}]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'www.example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('ignores disabled hosts even without certificate', () => {
|
||||
const certs: Certificate[] = []
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: true, certificate_id: null, enabled: false }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('ignores hosts without SSL forced', () => {
|
||||
const certs: Certificate[] = []
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: false, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calculates progress percentage with domain matching', () => {
|
||||
const certs: Certificate[] = [
|
||||
mockCertWithDomain('a.example.com'),
|
||||
mockCertWithDomain('b.example.com'),
|
||||
]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', domain_names: 'a.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', domain_names: 'b.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h3', domain_names: 'c.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h4', domain_names: 'd.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
// 2 out of 4 hosts have matching certs = 50%
|
||||
expect(screen.getByText('50% provisioned')).toBeInTheDocument()
|
||||
expect(screen.getByText('2 hosts awaiting certificate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows all pending when no certificates exist', () => {
|
||||
const certs: Certificate[] = []
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', domain_names: 'a.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', domain_names: 'b.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.getByText('2 hosts awaiting certificate')).toBeInTheDocument()
|
||||
expect(screen.getByText('0% provisioned')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows 100% provisioned when all SSL hosts have matching certificates', () => {
|
||||
const certs: Certificate[] = [
|
||||
mockCertWithDomain('a.example.com'),
|
||||
mockCertWithDomain('b.example.com'),
|
||||
]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', domain_names: 'a.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', domain_names: 'b.example.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
// Should NOT show awaiting indicator when all hosts are covered
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/provisioned/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles whitespace in domain names', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('example.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: ' example.com ', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles whitespace in certificate domains', () => {
|
||||
const certs: Certificate[] = [{
|
||||
...mockCertWithDomain('example.com'),
|
||||
domain: ' example.com '
|
||||
}]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, domain_names: 'example.com', ssl_forced: true, certificate_id: null, enabled: true }
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
expect(screen.queryByText(/awaiting certificate/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('correctly counts mix of covered and uncovered hosts', () => {
|
||||
const certs: Certificate[] = [mockCertWithDomain('covered.com')]
|
||||
const hosts: ProxyHost[] = [
|
||||
{ ...mockHost, uuid: 'h1', domain_names: 'covered.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h2', domain_names: 'uncovered.com', ssl_forced: true, certificate_id: null, enabled: true },
|
||||
{ ...mockHost, uuid: 'h3', domain_names: 'disabled.com', ssl_forced: true, certificate_id: null, enabled: false },
|
||||
{ ...mockHost, uuid: 'h4', domain_names: 'no-ssl.com', ssl_forced: false, certificate_id: null, enabled: true },
|
||||
]
|
||||
renderWithRouter(<CertificateStatusCard certificates={certs} hosts={hosts} />)
|
||||
|
||||
// Only h1 and h2 are SSL hosts that are enabled
|
||||
// h1 is covered, h2 is not
|
||||
expect(screen.getByText('1 host awaiting certificate')).toBeInTheDocument()
|
||||
expect(screen.getByText('50% provisioned')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user