From 018be2d54fe6c726d9492fd7e9366bcacfb25df4 Mon Sep 17 00:00:00 2001 From: Vladislav Schur Date: Tue, 2 Jun 2026 15:09:47 +0200 Subject: [PATCH 1/6] feat(aurora-portal): clavis create certificates Signed-off-by: Vladislav Schur --- .../IssueEndEntityCertificateModal.test.tsx | 107 ++++++++++++++++ .../IssueEndEntityCertificateModal.tsx | 114 ++++++++++++++++++ .../IssueSelfSignedCertificateModal.test.tsx | 112 +++++++++++++++++ .../IssueSelfSignedCertificateModal.tsx | 75 ++++++++++++ .../PcaCertificatesListContainer.test.tsx | 34 +++++- .../PcaCertificatesListContainer.tsx | 22 +++- .../-components/PcaDetailsView.test.tsx | 27 ++++- .../pca/$pcaId/-components/PcaDetailsView.tsx | 36 +++++- .../server/Services/routers/pcaRouter.test.ts | 88 +++++++++----- .../src/server/Services/routers/pcaRouter.ts | 9 +- .../src/server/Services/types/pca.test.ts | 72 +---------- .../aurora/src/server/Services/types/pca.ts | 14 +-- 12 files changed, 587 insertions(+), 123 deletions(-) create mode 100644 packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.test.tsx create mode 100644 packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.tsx create mode 100644 packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueSelfSignedCertificateModal.test.tsx create mode 100644 packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueSelfSignedCertificateModal.tsx diff --git a/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.test.tsx b/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.test.tsx new file mode 100644 index 000000000..71dab3931 --- /dev/null +++ b/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.test.tsx @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { act, render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { I18nProvider } from "@lingui/react" +import { i18n } from "@lingui/core" +import { PortalProvider } from "@cloudoperators/juno-ui-components" +import { IssueEndEntityCertificateModal } from "./IssueEndEntityCertificateModal" + +const mockProjectId = "project-123" +const mockMutateAsync = vi.fn().mockResolvedValue({}) +const mockReset = vi.fn() +const mockInvalidate = vi.fn() + +vi.mock("@/client/hooks", () => ({ + useProjectId: () => mockProjectId, +})) + +vi.mock("@/client/trpcClient", () => ({ + trpcReact: { + useUtils: () => ({ + services: { + pca: { + listCertificates: { + invalidate: mockInvalidate, + }, + }, + }, + }), + services: { + pca: { + createCertificate: { + useMutation: (options?: { onSettled?: () => void }) => ({ + isPending: false, + mutateAsync: async (input: unknown) => { + const result = await mockMutateAsync(input) + options?.onSettled?.() + return result + }, + reset: mockReset, + error: null, + }), + }, + }, + }, + }, +})) + +const renderModal = (onClose = vi.fn()) => + render( + + + + + + ) + +describe("IssueEndEntityCertificateModal", () => { + beforeEach(async () => { + vi.clearAllMocks() + await act(async () => { + i18n.activate("en") + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("does not submit when csr is empty", async () => { + const user = userEvent.setup() + + renderModal() + await user.click(screen.getByRole("button", { name: "Save" })) + + await waitFor(() => { + expect(mockMutateAsync).not.toHaveBeenCalled() + }) + }) + + it("submits normalized csr payload and closes modal", async () => { + const user = userEvent.setup() + const onClose = vi.fn() + vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000) + + renderModal(onClose) + + await user.type(screen.getByPlaceholderText("Paste CSR code"), "line1\\nline2") + await user.click(screen.getByRole("button", { name: "Save" })) + + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalledWith({ + project_id: "project-123", + certificate_authority_id: "ca-1", + csr: "line1\nline2", + configuration: { + validity: { + not_after: 1_700_028_800, + }, + }, + }) + }) + + expect(mockInvalidate).toHaveBeenCalledTimes(1) + expect(mockReset).toHaveBeenCalledTimes(1) + expect(onClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.tsx b/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.tsx new file mode 100644 index 000000000..9aabe49a7 --- /dev/null +++ b/packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.tsx @@ -0,0 +1,114 @@ +import { z } from "zod" +import { useForm, useStore } from "@tanstack/react-form" +import { Trans, useLingui } from "@lingui/react/macro" +import { Modal, Form, FormSection, Spinner, Message, Textarea } from "@cloudoperators/juno-ui-components" +import { trpcReact } from "@/client/trpcClient" +import { useProjectId } from "@/client/hooks" + +export interface IssueEndEntityCertificateModalProps { + open: boolean + onClose: () => void + pcaId: string +} + +export const IssueEndEntityCertificateModal = ({ open, onClose, pcaId }: IssueEndEntityCertificateModalProps) => { + const { t } = useLingui() + const projectId = useProjectId() + const utils = trpcReact.useUtils() + + const { isPending, ...createCertificateMutation } = trpcReact.services.pca.createCertificate.useMutation({ + onSettled: () => utils.services.pca.listCertificates.invalidate(), + }) + + const formSchema = z.object({ + csr: z.string().trim(), + }) + + const form = useForm({ + defaultValues: { + csr: "", + }, + validators: { + onSubmit: formSchema, + }, + onSubmit: async () => { + if (isPending) return + + await createCertificateMutation.mutateAsync({ + project_id: projectId, + certificate_authority_id: pcaId, + // Normalize to one format so users can paste raw multi-line CSRs with \n along with already formatted ones + csr: form.state.values.csr.replace(/\\n/g, "\n"), + configuration: { validity: { not_after: Math.floor(Date.now() / 1000) + 8 * 60 * 60 } }, + }) + handleClose() + }, + }) + + const handleClose = () => { + if (isPending) return + + form.reset() + createCertificateMutation.reset() + onClose() + } + + const currentCsr = useStore(form.store, (state) => state.values.csr) + + return ( + + {createCertificateMutation.error && ( + + {createCertificateMutation.error?.message} + + )} + + {isPending && ( +
+ + + Issuing End Entity Certificate... + +
+ )} + + {!isPending && ( +
{ + e.preventDefault() + form.handleSubmit() + }} + > + + ( +