Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/aurora-portal/docs/0011_clavis.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ Implemented screens and interactions:
- CA details page at `/projects/$projectId/services/pca/$pcaId/` via `PcaDetailsView`
- details page shows CA metadata, certificate validity, CSR content, and delete action
- details-page delete flow reuses the shared delete modal and redirects back to the PCA list after success
- certificate list view via `PcaCertificatesListContainer` displays certificates issued by a CA
- certificates list shows CA ID and certificate ID columns with loading, error, and empty states
- disabled "Issue End Entity Certificate" button (placeholder for future issue-certificate task)
- individual certificate rows rendered via `PcaCertificatesTableRow` component

The list page currently renders the CA state, id, and common name. It also shows the translated empty state when no PCAs are available for the current project.
The PCA list page renders the CA state, id, and common name with translated empty states when no PCAs are available for the current project.
The certificate list view integrates within the CA details view and fetches certificates via the `listCertificates` endpoint.

## Implemented BFF

Expand Down Expand Up @@ -79,7 +84,6 @@ Error states are surfaced directly in the modal or list view when the BFF call f

The backend already exposes certificate and import operations, but the UI does not yet have dedicated screens for:

- certificate list view
- certificate detail view
- certificate import flow
- list filtering, sorting, and search controls
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { i18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react"
import { PortalProvider } from "@cloudoperators/juno-ui-components"
import { describe, expect, it } from "vitest"
import type { Certificate } from "@/server/Services/types/pca"
import { PcaCertificatesTableRow } from "./PcaCertificatesTableRow"

const baseCertificate: Certificate = {
id: "cert-123",
certificate_authority_id: "ca-456",
project_id: "project-1",
}

const renderRow = (certificate: Certificate) =>
render(
<I18nProvider i18n={i18n}>
<PortalProvider>
<PcaCertificatesTableRow certificate={certificate} />
</PortalProvider>
</I18nProvider>
)

describe("PcaCertificatesTableRow", () => {
it("renders row with correct data-testid", () => {
renderRow(baseCertificate)

expect(screen.getByTestId("pca-certificate-row-cert-123")).toBeInTheDocument()
})

it("renders certificate_authority_id", () => {
renderRow(baseCertificate)

expect(screen.getByText("ca-456")).toBeInTheDocument()
})

it("renders certificate id", () => {
renderRow(baseCertificate)

expect(screen.getByText("cert-123")).toBeInTheDocument()
})

it("renders disabled Create Certificate menu item", async () => {
const user = userEvent.setup()
renderRow(baseCertificate)

await user.click(screen.getByRole("button", { name: "More" }))

const menuItem = screen.getByRole("menuitem", { name: "Create Certificate" })
expect(menuItem).toBeInTheDocument()
expect(menuItem).toHaveAttribute("aria-disabled", "true")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useLingui } from "@lingui/react/macro"
import {
DataGridCell,
DataGridRow,
PopupMenu,
PopupMenuItem,
PopupMenuOptions,
} from "@cloudoperators/juno-ui-components"
import { Certificate } from "@/server/Services/types/pca"

interface PcaCertificatesTableRowProps {
certificate: Certificate
}

export const PcaCertificatesTableRow = ({ certificate }: PcaCertificatesTableRowProps) => {
const { t } = useLingui()

// I will enable this button on get-by-id certificate task of the EPIC
// const navigateToDetailsPage = () =>
// navigate({
// to: "/projects/$projectId/services/pca/$pcaId",
// params: { projectId, pcaId: pca.id },
// })

return (
<DataGridRow key={certificate.id} data-testid={`pca-certificate-row-${certificate.id}`}>
<DataGridCell>{certificate.certificate_authority_id}</DataGridCell>
<DataGridCell>{certificate.id}</DataGridCell>
<DataGridCell onClick={(e) => e.stopPropagation()} className="items-end pr-0">
<PopupMenu>
<PopupMenuOptions>
{/* I will enable this button on create-certificate task of the EPIC */}
<PopupMenuItem label={t`Create Certificate`} disabled />
</PopupMenuOptions>
</PopupMenu>
</DataGridCell>
</DataGridRow>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { render, screen } from "@testing-library/react"
import { I18nProvider } from "@lingui/react"
import { i18n } from "@lingui/core"
import type { Certificate } from "@/server/Services/types/pca"
import { trpcReact } from "@/client/trpcClient"
import { PcaCertificatesListContainer } from "./PcaCertificatesListContainer"

vi.mock("@/client/trpcClient", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/client/trpcClient")>()
return {
...actual,
trpcReact: {
...actual.trpcReact,
services: {
...actual.trpcReact.services,
pca: {
...actual.trpcReact.services.pca,
listCertificates: {
...actual.trpcReact.services.pca.listCertificates,
useQuery: vi.fn(),
},
},
},
},
}
})

vi.mock("@/client/hooks", () => ({
useProjectId: () => "project-1",
}))

vi.mock("./-table/PcaCertificatesTableRow", () => ({
PcaCertificatesTableRow: ({ certificate }: { certificate: Certificate }) => (
<tr data-testid={`certificate-row-${certificate.id}`}>
<td>{certificate.certificate_authority_id}</td>
<td>{certificate.id}</td>
</tr>
),
}))

describe("PcaCertificatesListContainer", () => {
const validCertificate: Certificate = {
id: "cert-1",
certificate_authority_id: "ca-1",
project_id: "project-1",
}

const validCertificate2: Certificate = {
id: "cert-2",
certificate_authority_id: "ca-1",
project_id: "project-1",
}

beforeEach(() => {
vi.clearAllMocks()
})

const renderComponent = () =>
render(
<I18nProvider i18n={i18n}>
<PcaCertificatesListContainer pcaId="ca-1" />
</I18nProvider>
)

it("calls listCertificates query with correct parameters", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(vi.mocked(trpcReact.services.pca.listCertificates.useQuery)).toHaveBeenCalledWith({
project_id: "project-1",
certificate_authority_id: "ca-1",
})
})

it("renders loading state", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: true,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByText("Loading Certificates issued by Certificate Authority...")).toBeInTheDocument()
})

it("renders error state with error message", () => {
const errorMessage = "Failed to fetch certificates"
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: false,
isError: true,
error: { message: errorMessage },
} as never)

renderComponent()

expect(screen.getByText(errorMessage)).toBeInTheDocument()
})

it("renders default error message when no error details provided", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: false,
isError: true,
error: null,
} as never)

renderComponent()

expect(screen.getByText("Failed to load Certificates issued by Certificate Authority.")).toBeInTheDocument()
})

it("renders empty state when no certificates exist", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByText("No Certificates issued by this Certificate Authority found")).toBeInTheDocument()
expect(screen.getByText("There are no Certificates available for this Certificate Authority.")).toBeInTheDocument()
expect(screen.getByTestId("no-pcas-certificates")).toBeInTheDocument()
})

it("renders data grid with certificates", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [validCertificate, validCertificate2],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByTestId("certificate-row-cert-1")).toBeInTheDocument()
expect(screen.getByTestId("certificate-row-cert-2")).toBeInTheDocument()
})

it("renders disabled issue certificate button", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [validCertificate],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

const button = screen.getByRole("button", { name: "Issue End Entity Certificate" })
expect(button).toBeInTheDocument()
expect(button).toBeDisabled()
})

it("renders correct column headers", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [validCertificate],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByText("CA ID")).toBeInTheDocument()
expect(screen.getByText("ID")).toBeInTheDocument()
})

it("renders multiple certificates with different IDs", () => {
const cert3: Certificate = {
id: "cert-3",
certificate_authority_id: "ca-1",
project_id: "project-1",
}

vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [validCertificate, validCertificate2, cert3],
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByTestId("certificate-row-cert-1")).toBeInTheDocument()
expect(screen.getByTestId("certificate-row-cert-2")).toBeInTheDocument()
expect(screen.getByTestId("certificate-row-cert-3")).toBeInTheDocument()
})

it("uses default empty array when data is undefined", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
error: null,
} as never)

renderComponent()

expect(screen.getByText("No Certificates issued by this Certificate Authority found")).toBeInTheDocument()
})

it("renders with different pcaId prop", () => {
vi.mocked(trpcReact.services.pca.listCertificates.useQuery).mockReturnValue({
data: [],
isLoading: false,
isError: false,
error: null,
} as never)

const { rerender } = render(
<I18nProvider i18n={i18n}>
<PcaCertificatesListContainer pcaId="ca-1" />
</I18nProvider>
)

expect(vi.mocked(trpcReact.services.pca.listCertificates.useQuery)).toHaveBeenCalledWith({
project_id: "project-1",
certificate_authority_id: "ca-1",
})

rerender(
<I18nProvider i18n={i18n}>
<PcaCertificatesListContainer pcaId="ca-2" />
</I18nProvider>
)

expect(vi.mocked(trpcReact.services.pca.listCertificates.useQuery)).toHaveBeenCalledWith({
project_id: "project-1",
certificate_authority_id: "ca-2",
})
})
})
Loading