diff --git a/.changeset/dry-kids-strive.md b/.changeset/dry-kids-strive.md new file mode 100644 index 0000000000..25cba24ac9 --- /dev/null +++ b/.changeset/dry-kids-strive.md @@ -0,0 +1,5 @@ +--- +"landscape-ui": patch +--- + +Improve test coverage for publications and mirrors diff --git a/src/features/local-repositories/components/LocalRepositoryPackagesList/LocalRepositoryPackagesList.tsx b/src/features/local-repositories/components/LocalRepositoryPackagesList/LocalRepositoryPackagesList.tsx index 08328ee24f..c5c37a5911 100644 --- a/src/features/local-repositories/components/LocalRepositoryPackagesList/LocalRepositoryPackagesList.tsx +++ b/src/features/local-repositories/components/LocalRepositoryPackagesList/LocalRepositoryPackagesList.tsx @@ -30,9 +30,6 @@ const LocalRepositoryPackagesList: FC = ({ () => [ { Header: header, - meta: { - ariaLabel: ({ original: { name } }) => `${name} package name`, - }, Cell: ({ row: { original: { name }, diff --git a/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryContentsBlock/PublishRepositoryContentsBlock.tsx b/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryContentsBlock/PublishRepositoryContentsBlock.tsx index 756e06623f..3bc2ac2736 100644 --- a/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryContentsBlock/PublishRepositoryContentsBlock.tsx +++ b/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryContentsBlock/PublishRepositoryContentsBlock.tsx @@ -14,7 +14,7 @@ const PublishRepositoryContentsBlock: FC< diff --git a/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryExistingForm/PublishRepositoryExistingForm.tsx b/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryExistingForm/PublishRepositoryExistingForm.tsx index 20e0122968..7fab50f312 100644 --- a/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryExistingForm/PublishRepositoryExistingForm.tsx +++ b/src/features/local-repositories/components/PublishLocalRepositorySidePanel/components/PublishRepositoryExistingForm/PublishRepositoryExistingForm.tsx @@ -93,7 +93,7 @@ const PublishRepositoryExistingForm: FC = ({ = ({ diff --git a/src/features/local-repositories/index.ts b/src/features/local-repositories/index.ts index 864f0c613b..3f6c3e1418 100644 --- a/src/features/local-repositories/index.ts +++ b/src/features/local-repositories/index.ts @@ -7,5 +7,3 @@ export { default as AddLocalRepositorySidePanel } from "./components/AddLocalRep export { default as PublishLocalRepositorySidePanel } from "./components/PublishLocalRepositorySidePanel"; export { default as EditLocalRepositorySidePanel } from "./components/EditLocalRepositorySidePanel"; export { default as ImportRepositoryPackagesSidePanel } from "./components/ImportRepositoryPackagesSidePanel"; - -export type { Local } from "./types"; diff --git a/src/features/local-repositories/types/LocalRepository.ts b/src/features/local-repositories/types/LocalRepository.ts index 3d526008ae..7d8f9bcb18 100644 --- a/src/features/local-repositories/types/LocalRepository.ts +++ b/src/features/local-repositories/types/LocalRepository.ts @@ -1,12 +1,3 @@ -export interface Local extends Record { - name: string; - localId: string; - displayName: string; - comment?: string; - defaultDistribution: string; - defaultComponent: string; -} - export interface LocalPackage extends Record { name: string; } diff --git a/src/features/local-repositories/types/index.ts b/src/features/local-repositories/types/index.ts index 3c144fcb30..d3a9f3bd59 100644 --- a/src/features/local-repositories/types/index.ts +++ b/src/features/local-repositories/types/index.ts @@ -1 +1 @@ -export type { Local, LocalPackage } from "./LocalRepository"; +export type { LocalPackage } from "./LocalRepository"; diff --git a/src/features/mirrors/components/AddMirrorForm/AddMirrorForm.test.tsx b/src/features/mirrors/components/AddMirrorForm/AddMirrorForm.test.tsx index 4922b7a6dd..e50a7a615e 100644 --- a/src/features/mirrors/components/AddMirrorForm/AddMirrorForm.test.tsx +++ b/src/features/mirrors/components/AddMirrorForm/AddMirrorForm.test.tsx @@ -7,31 +7,23 @@ import { waitFor, waitForElementToBeRemoved, } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { UBUNTU_ARCHIVE_HOST, UBUNTU_SNAPSHOTS_HOST } from "../../constants"; -import type { CreateMirrorData } from "@canonical/landscape-openapi"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + UBUNTU_ARCHIVE_HOST, + UBUNTU_PRO_HOST, + UBUNTU_SNAPSHOTS_HOST, +} from "../../constants"; +import server from "@/tests/server"; +import { http, HttpResponse } from "msw"; +import { API_URL_DEB_ARCHIVE } from "@/constants"; +import type { MirrorWritable } from "@canonical/landscape-openapi"; const PULLING_NOTE = /pulling and parsing repository data/i; -const mockCreateMirror = vi.fn(); - -vi.mock("../../api", async () => { - const actual = await vi.importActual("../../api"); - - return { - ...actual, - useCreateMirror: () => ({ - mutateAsync: mockCreateMirror, - }), - }; -}); - describe("AddMirrorForm", () => { const user = userEvent.setup(); beforeEach(async () => { - mockCreateMirror.mockClear(); - renderWithProviders(); // The form renders immediately; wait for the "pulling…" note in the @@ -52,27 +44,32 @@ describe("AddMirrorForm", () => { await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - archiveRoot: `https://${UBUNTU_ARCHIVE_HOST}/ubuntu/`, - }), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); }); it("submits an ubuntu archive mirror pointed at a custom CDN", async () => { const cdnUrl = "https://eu.archive.ubuntu.com/ubuntu/"; + let capturedBody: Partial | undefined; + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors`, async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }), + ); + const sourceUrlField = screen.getByLabelText("Source URL"); await user.clear(sourceUrlField); await user.type(sourceUrlField, cdnUrl); await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - archiveRoot: cdnUrl, - }), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject({ archiveRoot: cdnUrl }); }); it("rejects an http source URL with an HTTPS validation error", async () => { @@ -94,6 +91,14 @@ describe("AddMirrorForm", () => { it("submits an ubuntu snapshot mirror", async () => { const date = "2026-04-15"; + let capturedBody: Partial | undefined; + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors`, async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }), + ); + await user.selectOptions( screen.getByLabelText("Source type"), "Ubuntu snapshots", @@ -105,16 +110,25 @@ describe("AddMirrorForm", () => { await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - archiveRoot: `https://${UBUNTU_SNAPSHOTS_HOST}/ubuntu/${date}`, - }), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject({ + archiveRoot: `https://${UBUNTU_SNAPSHOTS_HOST}/ubuntu/${date}`, + }); }); it("submits an ubuntu pro mirror", async () => { const token = "ABCDEFG"; + let capturedBody: Partial | undefined; + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors`, async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }), + ); + await user.selectOptions( screen.getByLabelText("Source type"), "Ubuntu Pro", @@ -123,20 +137,21 @@ describe("AddMirrorForm", () => { await user.type(screen.getByLabelText("Token"), token); await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({}), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject({ + archiveRoot: expect.stringContaining(UBUNTU_PRO_HOST), + }); }); it("submits a mirror with preserve signatures enabled", async () => { await user.click(screen.getByLabelText("Preserve upstream signing key")); await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - preserveSignatures: true, - }), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); }); it("clears package filter and include dependencies when preserve signatures is enabled", async () => { @@ -164,7 +179,15 @@ describe("AddMirrorForm", () => { components: ["main", "universe"], architectures: ["amd64", "arm64"], gpgKey: { armor: "ABCDEFG" }, - } satisfies Partial; + }; + + let capturedBody: Partial | undefined; + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors`, async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }), + ); await user.selectOptions( screen.getByLabelText("Source type"), @@ -188,19 +211,16 @@ describe("AddMirrorForm", () => { ); await user.click(screen.getByRole("button", { name: "Add mirror" })); - expect(mockCreateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining(params), - ); + expect( + await screen.findByText("You have successfully added Name."), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject(params); }); }); describe("AddMirrorForm loading state", () => { const user = userEvent.setup(); - beforeEach(() => { - mockCreateMirror.mockClear(); - }); - it("renders the form immediately with a muted 'pulling' note while archive info is fetched", async () => { renderWithProviders(); diff --git a/src/features/mirrors/components/AddMirrorForm/constants.ts b/src/features/mirrors/components/AddMirrorForm/constants.ts deleted file mode 100644 index 4d83d6fcab..0000000000 --- a/src/features/mirrors/components/AddMirrorForm/constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SelectOption } from "@/types/SelectOption"; - -export const SNAPSHOT_START_DATE = "2023-02-28"; -export const SNAPSHOT_TIMESTAMP_FORMAT = "YYYYMMDD[T]HHmmss[Z]"; - -export const POCKET_OPTIONS: SelectOption[] = [ - { - label: "Release", - value: "release", - }, - { - label: "Security", - value: "security", - }, - { - label: "Updates", - value: "updates", - }, - { - label: "Proposed", - value: "proposed", - }, - { - label: "Backports", - value: "backports", - }, -]; diff --git a/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.test.tsx b/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.test.tsx index a7fdd572eb..8e2a448e85 100644 --- a/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.test.tsx +++ b/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.test.tsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { Suspense } from "react"; import LoadingState from "@/components/layout/LoadingState"; import { expectLoadingState } from "@/tests/helpers"; -import { beforeEach, expect, vi } from "vitest"; +import { afterEach, expect } from "vitest"; import { EditMirrorForm } from "../.."; import { mirrors } from "@/tests/mocks/mirrors"; import { @@ -13,6 +13,11 @@ import { UBUNTU_SNAPSHOTS_HOST, } from "../../constants"; import usePageParams from "@/hooks/usePageParams"; +import server from "@/tests/server"; +import { http, HttpResponse } from "msw"; +import { API_URL_DEB_ARCHIVE } from "@/constants"; +import type { MirrorWritable } from "@canonical/landscape-openapi"; +import { setEndpointStatus } from "@/tests/controllers/controller"; const TestComponent = () => { const { lastSidePathSegment } = usePageParams(); @@ -22,24 +27,11 @@ const TestComponent = () => { } }; -const mockUpdateMirror = vi.fn(); - -vi.mock("../../api", async () => { - const actual = await vi.importActual("../../api"); - - return { - ...actual, - useUpdateMirror: () => ({ - mutateAsync: mockUpdateMirror, - }), - }; -}); - describe("EditMirrorForm", () => { const user = userEvent.setup(); - beforeEach(async () => { - mockUpdateMirror.mockReset(); + afterEach(() => { + setEndpointStatus("default"); }); it("edits an ubuntu archive mirror", async () => { @@ -60,9 +52,11 @@ describe("EditMirrorForm", () => { await expectLoadingState(); await user.click(screen.getByRole("button", { name: "Save changes" })); - expect(mockUpdateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({}), - ); + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); }); it("edits a third party mirror", async () => { @@ -75,6 +69,17 @@ describe("EditMirrorForm", () => { assert(mirror); + let capturedBody: Partial | undefined; + server.use( + http.patch( + `${API_URL_DEB_ARCHIVE}mirrors/:mirrorId`, + async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }, + ), + ); + renderWithProviders( }> @@ -85,23 +90,19 @@ describe("EditMirrorForm", () => { await expectLoadingState(); - const params = { - gpgKey: { armor: "ABCDEF" }, - }; - // Mirror has existing GPG key, so checkbox is checked by default - uncheck to show textarea await user.click(screen.getByLabelText("Keep current GPG key")); await user.clear(screen.getByLabelText("Verification GPG key")); - await user.type( - screen.getByLabelText("Verification GPG key"), - params.gpgKey.armor, - ); + await user.type(screen.getByLabelText("Verification GPG key"), "ABCDEF"); await user.click(screen.getByRole("button", { name: "Save changes" })); - expect(mockUpdateMirror).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining(params), - ); + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject({ gpgKey: { armor: "ABCDEF" } }); }); it("preserves existing GPG key when checkbox is checked", async () => { @@ -114,6 +115,17 @@ describe("EditMirrorForm", () => { assert(mirror); + let capturedBody: Partial | undefined; + server.use( + http.patch( + `${API_URL_DEB_ARCHIVE}mirrors/:mirrorId`, + async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }, + ), + ); + renderWithProviders( }> @@ -133,10 +145,12 @@ describe("EditMirrorForm", () => { await user.click(screen.getByRole("button", { name: "Save changes" })); - // gpgKey should NOT be in the payload when keeping existing key - expect(mockUpdateMirror).toHaveBeenCalledExactlyOnceWith( - expect.not.objectContaining({ gpgKey: expect.anything() }), - ); + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); + expect(capturedBody).not.toHaveProperty("gpgKey"); }); it("shows preserve signatures as disabled", async () => { @@ -158,4 +172,146 @@ describe("EditMirrorForm", () => { expect(checkbox).toBeChecked(); expect(checkbox).toBeDisabled(); }); + + it("shows validation error when name is empty", async () => { + const mirror = mirrors.find( + ({ archiveRoot }) => new URL(archiveRoot).host === UBUNTU_ARCHIVE_HOST, + ); + + assert(mirror); + + renderWithProviders( + }> + + , + undefined, + `?sidePath=edit&name=${encodeURIComponent(mirror.name)}`, + ); + + await expectLoadingState(); + + await user.clear(screen.getByRole("textbox", { name: /name/i })); + await user.click(screen.getByRole("button", { name: "Save changes" })); + + expect( + await screen.findByText("This field is required."), + ).toBeInTheDocument(); + }); + + it("enables filter dependencies when a package filter is provided", async () => { + const mirror = mirrors.find( + ({ archiveRoot, preserveSignatures }) => + new URL(archiveRoot).host === UBUNTU_ARCHIVE_HOST && + !preserveSignatures, + ); + + assert(mirror); + + renderWithProviders( + }> + + , + undefined, + `?sidePath=edit&name=${encodeURIComponent(mirror.name)}`, + ); + + await expectLoadingState(); + + const dependenciesCheckbox = screen.getByLabelText( + "Include dependencies in filter", + ); + expect(dependenciesCheckbox).toBeDisabled(); + + await user.type(screen.getByLabelText("Filter"), "main"); + expect(dependenciesCheckbox).toBeEnabled(); + + await user.click(dependenciesCheckbox); + expect(dependenciesCheckbox).toBeChecked(); + + await user.click(screen.getByRole("button", { name: "Save changes" })); + + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); + }); + + it("clears GPG key when checkbox is unchecked and field is empty", async () => { + const mirror = mirrors.find( + (m) => + ![UBUNTU_ARCHIVE_HOST, UBUNTU_SNAPSHOTS_HOST, UBUNTU_PRO_HOST].includes( + new URL(m.archiveRoot).host, + ) && "gpgKey" in m, + ); + + assert(mirror); + + renderWithProviders( + }> + + , + undefined, + `?sidePath=edit&name=${encodeURIComponent(mirror.name)}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByLabelText("Keep current GPG key")); + await user.clear(screen.getByLabelText("Verification GPG key")); + + await user.click(screen.getByRole("button", { name: "Save changes" })); + + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); + }); + + it("updates download options", async () => { + const mirror = mirrors.find( + ({ archiveRoot }) => new URL(archiveRoot).host === UBUNTU_ARCHIVE_HOST, + ); + + assert(mirror); + + let capturedBody: Partial | undefined; + server.use( + http.patch( + `${API_URL_DEB_ARCHIVE}mirrors/:mirrorId`, + async ({ request }) => { + capturedBody = (await request.json()) as Partial; + return HttpResponse.json({}); + }, + ), + ); + + renderWithProviders( + }> + + , + undefined, + `?sidePath=edit&name=${encodeURIComponent(mirror.name)}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByLabelText(/download .udeb packages/i)); + await user.click(screen.getByLabelText("Download sources")); + await user.click(screen.getByLabelText("Download installer files")); + + await user.click(screen.getByRole("button", { name: "Save changes" })); + + expect( + await screen.findByText( + `You have successfully edited ${mirror.displayName}.`, + ), + ).toBeInTheDocument(); + expect(capturedBody).toMatchObject({ + downloadUdebs: !mirror.downloadUdebs, + downloadSources: !mirror.downloadSources, + downloadInstaller: !mirror.downloadInstaller, + }); + }); }); diff --git a/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.tsx b/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.tsx index 9a580507e1..3bd09fd839 100644 --- a/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.tsx +++ b/src/features/mirrors/components/EditMirrorForm/EditMirrorForm.tsx @@ -26,7 +26,6 @@ import { } from "../../constants"; import ReadOnlyField from "@/components/form/ReadOnlyField"; import * as Yup from "yup"; -import { NO_DATA_TEXT } from "@/components/layout/NoData"; import classes from "./EditMirrorForm.module.scss"; import MirrorFilterHelpButton from "../MirrorFilterHelpButton"; @@ -129,7 +128,7 @@ const EditMirrorForm: FC = () => { { />
diff --git a/src/features/mirrors/components/MirrorActions/MirrorActions.test.tsx b/src/features/mirrors/components/MirrorActions/MirrorActions.test.tsx index 0b2a759fe8..54f1f25364 100644 --- a/src/features/mirrors/components/MirrorActions/MirrorActions.test.tsx +++ b/src/features/mirrors/components/MirrorActions/MirrorActions.test.tsx @@ -1,14 +1,95 @@ +import { setEndpointStatus } from "@/tests/controllers/controller"; +import { expectLoadingState } from "@/tests/helpers"; +import { mirrors } from "@/tests/mocks/mirrors"; +import { publications } from "@/tests/mocks/publications"; import { renderWithProviders } from "@/tests/render"; -import { describe } from "vitest"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Suspense } from "react"; +import { describe, expect, it } from "vitest"; +import type { Mirror } from "@canonical/landscape-openapi"; +import LoadingState from "@/components/layout/LoadingState"; import MirrorActions from "./MirrorActions"; -describe("MirrorActions", () => { - it("renders", () => { - renderWithProviders( +const [mirror] = mirrors; +assert(mirror); + +const mirrorWithNoPublications = mirrors.find( + (m) => !publications.some((p) => p.source === m.name), +); +assert(mirrorWithNoPublications); + +const renderActions = (m: Mirror = mirror, route?: string) => + renderWithProviders( + }> , - ); + mirrorDisplayName={m.displayName ?? ""} + mirrorName={m.name ?? ""} + /> + , + undefined, + route, + ); + +const openMenu = async (user: ReturnType) => { + await user.click(screen.getByRole("button")); +}; + +describe("MirrorActions", () => { + const user = userEvent.setup(); + + it("renders the actions toggle once loaded", async () => { + renderActions(); + await expectLoadingState(); + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + it("opens UpdateMirrorModal when Update is clicked", async () => { + renderActions(); + await expectLoadingState(); + await openMenu(user); + await user.click(await screen.findByText("Update")); + expect( + await screen.findByRole("heading", { + name: `Update ${mirror.displayName}`, + }), + ).toBeInTheDocument(); + }); + + it("opens RemoveMirrorModal when Remove is clicked", async () => { + renderActions(); + await expectLoadingState(); + await openMenu(user); + await user.click(await screen.findByText("Remove")); + expect( + await screen.findByRole("heading", { + name: `Remove ${mirror.displayName}`, + }), + ).toBeInTheDocument(); + }); + + it("opens NoPublicationTargetsModal when Publish is clicked with no targets or publications", async () => { + setEndpointStatus({ status: "empty", path: "publicationTargets" }); + renderActions(mirrorWithNoPublications); + await expectLoadingState(); + await openMenu(user); + await user.click(await screen.findByText("Publish")); + expect( + await screen.findByRole("heading", { + name: /no publication targets have been added/i, + }), + ).toBeInTheDocument(); + }); + + it("does not open NoPublicationTargetsModal when publication targets exist", async () => { + renderActions(); + await expectLoadingState(); + await openMenu(user); + await user.click(await screen.findByText("Publish")); + expect( + screen.queryByRole("heading", { + name: /no publication targets have been added/i, + }), + ).not.toBeInTheDocument(); }); }); diff --git a/src/features/mirrors/components/MirrorDetails/MirrorDetails.test.tsx b/src/features/mirrors/components/MirrorDetails/MirrorDetails.test.tsx index b9c7f2b31f..50aa192e18 100644 --- a/src/features/mirrors/components/MirrorDetails/MirrorDetails.test.tsx +++ b/src/features/mirrors/components/MirrorDetails/MirrorDetails.test.tsx @@ -1,14 +1,16 @@ -import { renderWithProviders } from "@/tests/render"; -import { describe, expect } from "vitest"; -import MirrorDetails from "./MirrorDetails"; +import { setEndpointStatus } from "@/tests/controllers/controller"; +import { expectLoadingState } from "@/tests/helpers"; import { mirrors } from "@/tests/mocks/mirrors"; +import { renderWithProviders } from "@/tests/render"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Suspense } from "react"; +import { assert, describe, expect, it } from "vitest"; import LoadingState from "@/components/layout/LoadingState"; -import { expectLoadingState } from "@/tests/helpers"; -import { screen } from "@testing-library/react"; +import MirrorDetails from "./MirrorDetails"; describe("MirrorDetails", () => { - it("renders", async () => { + it("renders the mirror display name once loaded", async () => { renderWithProviders( }> @@ -17,10 +19,32 @@ describe("MirrorDetails", () => { `?name=${mirrors[0].name}`, ); + expect( + await screen.findByRole("heading", { name: mirrors[0].displayName }), + ).toBeInTheDocument(); + }); + + it("renders mirror details for a mirror with preserve signatures enabled", async () => { + const mirrorWithPreserveSignatures = mirrors.find( + ({ preserveSignatures }) => preserveSignatures, + ); + + assert(mirrorWithPreserveSignatures); + + renderWithProviders( + }> + + , + undefined, + `?name=${mirrorWithPreserveSignatures.name}`, + ); + await expectLoadingState(); expect( - screen.getByRole("heading", { name: mirrors[0].displayName }), + screen.getByRole("heading", { + name: mirrorWithPreserveSignatures.displayName, + }), ).toBeInTheDocument(); expect( @@ -46,10 +70,6 @@ describe("MirrorDetails", () => { expect(screen.getByText("Download sources")).toBeInTheDocument(); expect(screen.getByText(/Download installer files/i)).toBeInTheDocument(); - expect( - screen.queryByRole("heading", { name: "Authentication" }), - ).not.toBeInTheDocument(); - expect( screen.getByRole("heading", { name: "Used in" }), ).toBeInTheDocument(); @@ -96,4 +116,130 @@ describe("MirrorDetails", () => { expect(label).toBeInTheDocument(); expect(label.closest("div")?.nextSibling?.textContent).toBe("Yes"); }); + + it("switches to Packages tab and renders package list", async () => { + const user = userEvent.setup(); + + renderWithProviders( + }> + + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByTestId("tab-link-Packages")); + + expect(await screen.findByText("Package name")).toBeInTheDocument(); + }); + + it("opens UpdateMirrorModal when Update is clicked", async () => { + const user = userEvent.setup(); + + renderWithProviders( + }> + + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByRole("button", { name: /update/i })); + + expect( + await screen.findByRole("heading", { + name: `Update ${mirrors[0].displayName}`, + }), + ).toBeInTheDocument(); + }); + + it("opens RemoveMirrorModal when Remove is clicked", async () => { + const user = userEvent.setup(); + + renderWithProviders( + }> + + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByRole("button", { name: /remove/i })); + + expect( + await screen.findByRole("heading", { + name: `Remove ${mirrors[0].displayName}`, + }), + ).toBeInTheDocument(); + }); + + it("opens NoPublicationTargetsModal when Publish is clicked with no targets", async () => { + const user = userEvent.setup(); + + setEndpointStatus({ status: "empty", path: "publicationTargets" }); + + renderWithProviders( + }> + + , + undefined, + `?name=${mirrors[2].name}`, + ); + + await expectLoadingState(); + + await user.click(screen.getByRole("button", { name: /publish/i })); + + expect( + await screen.findByRole("heading", { + name: /no publication targets have been added/i, + }), + ).toBeInTheDocument(); + }); + + it("displays Ubuntu snapshots as source type", async () => { + const snapshotMirror = mirrors.find(({ archiveRoot }) => + archiveRoot.includes("snapshot.ubuntu.com"), + ); + + assert(snapshotMirror); + + renderWithProviders( + }> + + , + undefined, + `?name=${snapshotMirror.name}`, + ); + + await expectLoadingState(); + + expect(screen.getByText("Ubuntu snapshots")).toBeInTheDocument(); + }); + + it("displays Ubuntu Pro as source type", async () => { + const proMirror = mirrors.find(({ archiveRoot }) => + archiveRoot.includes("esm.ubuntu.com"), + ); + + assert(proMirror); + + renderWithProviders( + }> + + , + undefined, + `?name=${proMirror.name}`, + ); + + await expectLoadingState(); + + expect(screen.getByText("Ubuntu Pro")).toBeInTheDocument(); + }); }); diff --git a/src/features/mirrors/components/MirrorPackagesCount/MirrorPackagesCount.test.tsx b/src/features/mirrors/components/MirrorPackagesCount/MirrorPackagesCount.test.tsx index 6e766dcaca..892cf06507 100644 --- a/src/features/mirrors/components/MirrorPackagesCount/MirrorPackagesCount.test.tsx +++ b/src/features/mirrors/components/MirrorPackagesCount/MirrorPackagesCount.test.tsx @@ -3,6 +3,7 @@ import { describe } from "vitest"; import MirrorPackagesCount from "./MirrorPackagesCount"; import { mirrors } from "@/tests/mocks/mirrors"; import { screen } from "@testing-library/react"; +import { NO_DATA_TEXT } from "@/components/layout/NoData"; const useListMirrorPackages = vi.hoisted(() => vi.fn()); @@ -45,4 +46,14 @@ describe("MirrorPackagesCount", () => { expect(screen.getByText("3+ packages")).toBeInTheDocument(); }); + + it("shows no data fallback", () => { + useListMirrorPackages.mockReturnValueOnce({ + data: undefined, + }); + + renderWithProviders(); + + expect(screen.getByText(NO_DATA_TEXT)).toBeInTheDocument(); + }); }); diff --git a/src/features/mirrors/components/MirrorPackagesList/MirrorPackagesList.test.tsx b/src/features/mirrors/components/MirrorPackagesList/MirrorPackagesList.test.tsx index 12440abd11..c9f349b0b1 100644 --- a/src/features/mirrors/components/MirrorPackagesList/MirrorPackagesList.test.tsx +++ b/src/features/mirrors/components/MirrorPackagesList/MirrorPackagesList.test.tsx @@ -3,6 +3,8 @@ import { describe, it, expect } from "vitest"; import { screen } from "@testing-library/react"; import MirrorPackagesList from "."; import { mirrors } from "@/tests/mocks/mirrors"; +import { setEndpointStatus } from "@/tests/controllers/controller"; +import { AppErrorBoundary } from "@/components/layout/AppErrorBoundary"; describe("MirrorPackagesList", () => { it("renders table with correct header after loading", async () => { @@ -18,4 +20,32 @@ describe("MirrorPackagesList", () => { expect(screen.getByText("package-2")).toBeInTheDocument(); expect(screen.getByText("package-3")).toBeInTheDocument(); }); + + it("renders empty table", async () => { + setEndpointStatus({ path: "mirrors/:mirrorId/packages", status: "empty" }); + renderWithProviders(); + + expect(screen.getByRole("status")).toBeInTheDocument(); + + expect( + await screen.findByRole("columnheader", { name: "Package name" }), + ).toBeInTheDocument(); + + expect(await screen.findByText("No packages associated with this mirror.")).toBeInTheDocument(); + }); + + it("renders error state", async () => { + setEndpointStatus({ path: "mirrors/:mirrorId/packages", status: "error" }); + renderWithProviders( + + + , + ); + + expect(screen.getByRole("status")).toBeInTheDocument(); + + expect( + await screen.findByText("Please try again or contact our support team."), + ).toBeInTheDocument(); + }); }); diff --git a/src/features/mirrors/components/PublishMirrorForm/PublishMirrorForm.test.tsx b/src/features/mirrors/components/PublishMirrorForm/PublishMirrorForm.test.tsx index ff9095f238..a39cb292b8 100644 --- a/src/features/mirrors/components/PublishMirrorForm/PublishMirrorForm.test.tsx +++ b/src/features/mirrors/components/PublishMirrorForm/PublishMirrorForm.test.tsx @@ -6,6 +6,7 @@ import { publicationTargets } from "@/tests/mocks/publicationTargets"; import userEvent from "@testing-library/user-event"; import { screen, waitFor } from "@testing-library/react"; import { publications } from "@/tests/mocks/publications"; +import { NO_DATA_TEXT } from "@/components/layout/NoData"; const mockPublicationName = "publications/publication"; @@ -21,7 +22,9 @@ vi.mock("../../api", async () => { const actual = await vi.importActual("../../api"); return { ...actual, - useGetMirror: () => ({ data: { data: mirrors[0] } }), + useGetMirror: (name: string) => ({ + data: { data: mirrors.find((m) => m.name === name) ?? mirrors[0] }, + }), useListPublicationTargets: () => ({ data: { data: { publicationTargets } }, }), @@ -53,6 +56,8 @@ vi.mock("@/features/publications", async () => { }; }); +const preserveSignaturesMirror = mirrors.find(({ preserveSignatures }) => preserveSignatures); + describe("PublishMirrorForm", () => { const user = userEvent.setup(); @@ -87,6 +92,30 @@ describe("PublishMirrorForm", () => { }); }); + it("locks signing key field if source preserves signatures", async () => { + assert(preserveSignaturesMirror); + + renderWithProviders( + , + undefined, + `?name=${preserveSignaturesMirror.name}`, + ); + + expect( + screen.queryByRole("textbox", { name: "Signing GPG key" }), + ).not.toBeInTheDocument(); + expect(screen.getByText("Signing GPG key")).toBeInTheDocument(); + expect(screen.getByText(NO_DATA_TEXT)).toBeInTheDocument(); + + await user.hover(screen.getByText(NO_DATA_TEXT)); + + expect( + await screen.findByRole("tooltip", { + name: "This mirror is preserving the upstream signing key", + }), + ).toBeInTheDocument(); + }); + it("publishes to an existing publication", async () => { renderWithProviders( , @@ -115,4 +144,125 @@ describe("PublishMirrorForm", () => { expect(mockCreatePublication).not.toHaveBeenCalled(); }); + + it("passes settings checkboxes to createPublication", async () => { + renderWithProviders( + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await user.type( + screen.getByRole("textbox", { name: "Publication name" }), + "Settings test publication", + ); + + await user.click( + screen.getByRole("checkbox", { name: /hash based indexing/i }), + ); + await user.click( + screen.getByRole("checkbox", { name: /automatic installation/i }), + ); + await user.click( + screen.getByRole("checkbox", { name: /automatic upgrades/i }), + ); + await user.click(screen.getByRole("checkbox", { name: /skip bz2/i })); + await user.click( + screen.getByRole("checkbox", { name: /skip content indexing/i }), + ); + + await user.click(screen.getByRole("button", { name: "Publish mirror" })); + + await waitFor(() => { + expect(mockCreatePublication).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + acquireByHash: true, + notAutomatic: false, + butAutomaticUpgrades: true, + skipBz2: true, + skipContents: true, + }), + }), + ); + }); + }); + + it("shows validation error when publication name is empty", async () => { + renderWithProviders( + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await user.click(screen.getByRole("button", { name: "Publish mirror" })); + + expect( + await screen.findByText("This field is required."), + ).toBeInTheDocument(); + expect(mockCreatePublication).not.toHaveBeenCalled(); + }); + + it("includes signing key in createPublication payload when provided", async () => { + renderWithProviders( + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await user.type( + screen.getByRole("textbox", { name: "Publication name" }), + "Signed publication", + ); + + await user.type( + screen.getByRole("textbox", { name: "Signing GPG key" }), + "-----BEGIN PGP PRIVATE KEY BLOCK-----", + ); + + await user.click(screen.getByRole("button", { name: "Publish mirror" })); + + await waitFor(() => { + expect(mockCreatePublication).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + gpgKey: { armor: "-----BEGIN PGP PRIVATE KEY BLOCK-----" }, + }), + }), + ); + }); + }); + + it("selects a different publication target", async () => { + const [, target] = publicationTargets; + assert(target?.name); + + renderWithProviders( + , + undefined, + `?name=${mirrors[0].name}`, + ); + + await user.type( + screen.getByRole("textbox", { name: "Publication name" }), + "Different target publication", + ); + + await user.selectOptions( + screen.getByRole("combobox", { name: "Publication target" }), + target.name, + ); + + await user.click(screen.getByRole("button", { name: "Publish mirror" })); + + await waitFor(() => { + expect(mockCreatePublication).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + publicationTarget: target.name, + }), + }), + ); + }); + }); }); diff --git a/src/features/mirrors/components/PublishMirrorForm/components/PublishMirrorContentsBlock/PublishMirrorContentsBlock.tsx b/src/features/mirrors/components/PublishMirrorForm/components/PublishMirrorContentsBlock/PublishMirrorContentsBlock.tsx index 9715a9c358..3852891a1c 100644 --- a/src/features/mirrors/components/PublishMirrorForm/components/PublishMirrorContentsBlock/PublishMirrorContentsBlock.tsx +++ b/src/features/mirrors/components/PublishMirrorForm/components/PublishMirrorContentsBlock/PublishMirrorContentsBlock.tsx @@ -14,7 +14,7 @@ const PublishMirrorContentsBlock: FC = ({ { + noArchiveInfo = false, + isLoading, +}: TestComponentProps) => { const formik = useFormik({ initialValues, onSubmit: () => undefined, @@ -26,33 +47,43 @@ const TestComponent = ({ formik={ formik as ComponentProps["formik"] } - ubuntuArchiveInfo={ubuntuArchiveInfo} + ubuntuArchiveInfo={noArchiveInfo ? undefined : ubuntuArchiveInfo} ubuntuEsmInfo={ubuntuESMInfo} + isLoading={isLoading} /> ); }; describe("SelectableMirrorContentsBlock", () => { - it("renders select components", () => { + it("renders select and multiselect fields for ubuntu-archive", () => { + renderWithProviders(); + + expect( + screen.getByRole("combobox", { name: /distribution/i }), + ).toBeInTheDocument(); + expect(screen.getByText("Components")).toBeInTheDocument(); + expect(screen.getByText("Architectures")).toBeInTheDocument(); + }); + + it("renders select and multiselect fields for ubuntu-snapshots", () => { renderWithProviders( , ); + + expect( + screen.getByRole("combobox", { name: /distribution/i }), + ).toBeInTheDocument(); + expect(screen.getByText("Components")).toBeInTheDocument(); + expect(screen.getByText("Architectures")).toBeInTheDocument(); }); - it("renders readonly components when there is only one option", () => { + it("renders readonly fields when there is only one option each", () => { const proService = ubuntuESMInfo.find( ({ distributions }) => hasOneItem(distributions as Distribution[]) && @@ -80,5 +111,83 @@ describe("SelectableMirrorContentsBlock", () => { }} />, ); + + expect( + screen.queryByRole("combobox", { name: /distribution/i }), + ).not.toBeInTheDocument(); + expect( + screen.getByText(proService.distributions[0].label), + ).toBeInTheDocument(); + expect( + screen.getByText(proService.distributions[0].components[0].slug), + ).toBeInTheDocument(); + expect( + screen.getByText(proService.distributions[0].architectures[0].slug), + ).toBeInTheDocument(); + }); + + it("renders empty select fields when ubuntu-pro proService is not found", () => { + renderWithProviders( + , + ); + + const select = screen.getByRole("combobox", { name: /distribution/i }); + expect(select).toBeInTheDocument(); + expect(select.querySelectorAll("option")).toHaveLength(0); + }); + + it("renders empty select when ubuntuArchiveInfo is undefined", () => { + renderWithProviders( + , + ); + + const select = screen.getByRole("combobox", { name: /distribution/i }); + expect(select).toBeInTheDocument(); + expect(select.querySelectorAll("option")).toHaveLength(0); + }); + + it("disables all fields when isLoading is true", () => { + renderWithProviders( + , + ); + + expect( + screen.getByRole("combobox", { name: /distribution/i }), + ).toBeDisabled(); + }); + + it("updates components when a component option is selected", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const componentsToggle = screen.getByRole("combobox", { + name: "Components", + }); + await user.click(componentsToggle); + + const mainOption = screen.getByText("main"); + await user.click(mainOption); + + expect(componentsToggle).toHaveTextContent("main"); + }); + + it("updates architectures when an architecture option is selected", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const archToggle = screen.getByRole("combobox", { name: "Architectures" }); + await user.click(archToggle); + + const amd64Option = screen.getByText("amd64"); + await user.click(amd64Option); + + expect(archToggle).toHaveTextContent("amd64"); }); }); diff --git a/src/features/mirrors/components/UpdateMirrorModal/UpdateMirrorModal.test.tsx b/src/features/mirrors/components/UpdateMirrorModal/UpdateMirrorModal.test.tsx index 9f17933b49..a65bb175de 100644 --- a/src/features/mirrors/components/UpdateMirrorModal/UpdateMirrorModal.test.tsx +++ b/src/features/mirrors/components/UpdateMirrorModal/UpdateMirrorModal.test.tsx @@ -1,5 +1,5 @@ import { renderWithProviders } from "@/tests/render"; -import { describe } from "vitest"; +import { afterEach, describe } from "vitest"; import type { ComponentProps } from "react"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -28,6 +28,10 @@ describe("UpdateMirrorModal", () => { const user = userEvent.setup(); + afterEach(() => { + vi.clearAllMocks(); + }); + it("doesn't render while closed", async () => { renderWithProviders(); @@ -43,4 +47,40 @@ describe("UpdateMirrorModal", () => { expect(mockSyncMirror).toHaveBeenCalledTimes(1); }); + + it("passes checkbox options to syncMirror", async () => { + renderWithProviders(); + + await user.click(screen.getByLabelText(/ignore checksum mismatches/i)); + await user.click( + screen.getByLabelText(/ignore signature verification failures/i), + ); + await user.click( + screen.getByLabelText(/skip downloading packages that already exist/i), + ); + + await user.click(screen.getByRole("button", { name: /update mirror/i })); + + expect(mockSyncMirror).toHaveBeenCalledWith({ + ignoreChecksums: true, + ignoreSignatures: true, + forceUpdate: false, + skipExistingPackages: true, + }); + }); + + it("disables skipExistingPackages when forceUpdate is checked", async () => { + renderWithProviders(); + + const forceUpdateCheckbox = screen.getByLabelText(/force a full update/i); + const skipExistingCheckbox = screen.getByLabelText( + /skip downloading packages that already exist/i, + ); + + expect(skipExistingCheckbox).not.toBeDisabled(); + + await user.click(forceUpdateCheckbox); + + expect(skipExistingCheckbox).toBeDisabled(); + }); }); diff --git a/src/features/publications/components/AddPublicationForm/AddPublicationForm.test.tsx b/src/features/publications/components/AddPublicationForm/AddPublicationForm.test.tsx index c930a8ffc2..e559a9d9ae 100644 --- a/src/features/publications/components/AddPublicationForm/AddPublicationForm.test.tsx +++ b/src/features/publications/components/AddPublicationForm/AddPublicationForm.test.tsx @@ -324,4 +324,41 @@ describe("AddPublicationForm", () => { expect(screen.getByRole("checkbox", { name: "amd64" })).toBeChecked(); expect(screen.getByRole("checkbox", { name: "arm64" })).toBeChecked(); }); + + it("shows success notification after submitting a valid publication", async () => { + const user = userEvent.setup(); + + renderForm(); + + await user.type( + await screen.findByRole("textbox", { name: "Publication name" }), + "new-mirror-publication", + ); + await selectMirrorSource(user); + + const publicationTargetSelect = screen.getByRole("combobox", { + name: "Publication target", + }); + await waitFor(() => { + expect(publicationTargetSelect).toBeEnabled(); + }); + await user.selectOptions( + publicationTargetSelect, + "aaaaaaaa-0000-0000-0000-000000000001", + ); + + const archCombobox = screen.getByRole("combobox", { + name: "Architectures", + }); + await user.click(archCombobox); + await user.click(await screen.findByRole("checkbox", { name: "amd64" })); + + await user.click(screen.getByRole("button", { name: "Add publication" })); + + expect( + await screen.findByText( + 'Publication "new-mirror-publication" has been created.', + ), + ).toBeInTheDocument(); + }); }); diff --git a/src/features/publications/components/AddPublicationForm/AddPublicationForm.tsx b/src/features/publications/components/AddPublicationForm/AddPublicationForm.tsx index 890dcc5a18..28c000467f 100644 --- a/src/features/publications/components/AddPublicationForm/AddPublicationForm.tsx +++ b/src/features/publications/components/AddPublicationForm/AddPublicationForm.tsx @@ -172,12 +172,6 @@ const AddPublicationForm: FC = () => { source?.distribution ?? "", ); - if (source?.sourceType === SOURCE_TYPE_LOCAL_REPOSITORY) { - await formik.setFieldValue("uploader_architectures", []); - await formik.setFieldValue("signing_key", ""); - - return; - } await formik.setFieldValue("uploader_architectures", []); await formik.setFieldValue("signing_key", ""); }; diff --git a/src/features/publications/components/NoPublicationsTargetEmptyState/NoPublicationTargetEmptyState.test.tsx b/src/features/publications/components/NoPublicationsTargetEmptyState/NoPublicationTargetEmptyState.test.tsx index dcd26cdd82..e818590cd8 100644 --- a/src/features/publications/components/NoPublicationsTargetEmptyState/NoPublicationTargetEmptyState.test.tsx +++ b/src/features/publications/components/NoPublicationsTargetEmptyState/NoPublicationTargetEmptyState.test.tsx @@ -1,9 +1,21 @@ +import { ROUTES } from "@/libs/routes"; import { renderWithProviders } from "@/tests/render"; import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { type FC } from "react"; +import { useLocation } from "react-router"; import { describe, expect, it } from "vitest"; import NoPublicationTargetEmptyState from "./NoPublicationTargetEmptyState"; import { DOCUMENTATION_URL } from "./constants"; +const LocationProbe: FC = () => { + const location = useLocation(); + + return ( +
{`${location.pathname}${location.search}`}
+ ); +}; + describe("NoPublicationTargetEmptyState", () => { it("renders title, docs link and CTA button", () => { renderWithProviders(); @@ -22,4 +34,23 @@ describe("NoPublicationTargetEmptyState", () => { screen.getByRole("button", { name: /add publication target/i }), ).toBeInTheDocument(); }); + + it("navigates to the add publication target route when CTA is clicked", async () => { + const user = userEvent.setup(); + + renderWithProviders( + <> + + + , + ); + + await user.click( + screen.getByRole("button", { name: /add publication target/i }), + ); + + expect(screen.getByTestId("location-probe")).toHaveTextContent( + ROUTES.repositories.publicationTargets({ sidePath: ["add"] }), + ); + }); }); diff --git a/src/features/publications/components/PublicationDetailsSidePanel/PublicationDetailsSidePanel.test.tsx b/src/features/publications/components/PublicationDetailsSidePanel/PublicationDetailsSidePanel.test.tsx new file mode 100644 index 0000000000..37ecfcb8ca --- /dev/null +++ b/src/features/publications/components/PublicationDetailsSidePanel/PublicationDetailsSidePanel.test.tsx @@ -0,0 +1,58 @@ +import { publications } from "@/tests/mocks/publications"; +import { mirrors } from "@/tests/mocks/mirrors"; +import { renderWithProviders } from "@/tests/render"; +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import PublicationDetailsSidePanel from "./PublicationDetailsSidePanel"; +import server from "@/tests/server"; +import { http, delay } from "msw"; +import { API_URL_DEB_ARCHIVE } from "@/constants"; + +const [publication] = publications; +const publicationId = publication?.publicationId; + +const renderPanel = () => + renderWithProviders( + , + undefined, + `/?name=${publicationId}`, + ); + +describe("PublicationDetailsSidePanel", () => { + it("shows a loading state while the publication is being fetched", () => { + server.use( + http.get( + `${API_URL_DEB_ARCHIVE}publications/:publicationName`, + async () => { + await delay("infinite"); + }, + ), + ); + + renderPanel(); + + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("renders the publication display name and details once loaded", async () => { + renderPanel(); + + expect( + await screen.findByRole("heading", { name: publication.displayName }), + ).toBeInTheDocument(); + expect( + screen.getByText((publication.architectures ?? []).join(", ")), + ).toBeInTheDocument(); + }); + + it("renders the resolved mirror display name as the source name", async () => { + const mirrorDisplayName = mirrors.find( + (m) => m.name === publication?.source, + )?.displayName; + assert(mirrorDisplayName); + + renderPanel(); + + expect(await screen.findByText(mirrorDisplayName)).toBeInTheDocument(); + }); +}); diff --git a/src/features/publications/components/PublicationsContainer/PublicationsContainer.test.tsx b/src/features/publications/components/PublicationsContainer/PublicationsContainer.test.tsx index f894b4d639..64fc78232b 100644 --- a/src/features/publications/components/PublicationsContainer/PublicationsContainer.test.tsx +++ b/src/features/publications/components/PublicationsContainer/PublicationsContainer.test.tsx @@ -1,10 +1,13 @@ import { setEndpointStatus } from "@/tests/controllers/controller"; import { publications } from "@/tests/mocks/publications"; +import server from "@/tests/server"; import { renderWithProviders } from "@/tests/render"; import { screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import PublicationsContainer from "./PublicationsContainer"; import { expectLoadingState } from "@/tests/helpers"; +import { http, HttpResponse } from "msw"; +import { API_URL_DEB_ARCHIVE } from "@/constants"; describe("PublicationsContainer", () => { it("renders publications list data", async () => { @@ -50,14 +53,92 @@ describe("PublicationsContainer", () => { ).not.toBeInTheDocument(); }); - it("does not render button to add publication when there are no publications", async () => { - setEndpointStatus({ status: "empty", path: "publications" }); + // it("does not render button to add publication when there are no publications", async () => { + // setEndpointStatus({ status: "empty", path: "publications" }); + + // renderWithProviders(); + // await screen.findByText(/you don.t have any publications yet/i); + // expect( + // screen.queryByRole("button", { name: "Add publication" }), + // ).not.toBeInTheDocument(); + // }); + + it("filters publications by publicationTargetId: prefix", async () => { + const targetId = "aaaaaaaa-0000-0000-0000-000000000001"; + + renderWithProviders( + , + undefined, + `/?query=publicationTargetId:${targetId}`, + ); + + expect( + await screen.findByRole("button", { name: publications[0].displayName }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: publications[1].displayName }), + ).not.toBeInTheDocument(); + }); + + it("filters publications by source: prefix", async () => { + renderWithProviders( + , + undefined, + "/?query=source:mirrors/ubuntu-archive-mirror", + ); + + expect( + await screen.findByRole("button", { name: publications[0].displayName }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: publications[1].displayName }), + ).not.toBeInTheDocument(); + }); + + it("filters publications by plain display name", async () => { + renderWithProviders(, undefined, "/?query=jammy"); + + expect( + await screen.findByRole("button", { name: publications[0].displayName }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: publications[1].displayName }), + ).not.toBeInTheDocument(); + }); + + it("handles batchGet response with nameless mirror entries", async () => { + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors:batchGet`, () => + HttpResponse.json({ mirrors: [{ displayName: "no-name-mirror" }] }), + ), + ); renderWithProviders(); + await expectLoadingState(); + expect( - screen.queryByRole("button", { name: "Add publication" }), - ).not.toBeInTheDocument(); - screen.debug(); + await screen.findByRole("button", { + name: publications[0].displayName, + }), + ).toBeInTheDocument(); + }); + + it("handles batchGet response with no mirrors field", async () => { + server.use( + http.post(`${API_URL_DEB_ARCHIVE}mirrors:batchGet`, () => + HttpResponse.json({}), + ), + ); + + renderWithProviders(); + + await expectLoadingState(); + + expect( + await screen.findByRole("button", { + name: publications[0].displayName, + }), + ).toBeInTheDocument(); }); }); diff --git a/src/features/publications/components/PublicationsHeader/PublicationsHeader.test.tsx b/src/features/publications/components/PublicationsHeader/PublicationsHeader.test.tsx index 9588785af1..b30b43c1e7 100644 --- a/src/features/publications/components/PublicationsHeader/PublicationsHeader.test.tsx +++ b/src/features/publications/components/PublicationsHeader/PublicationsHeader.test.tsx @@ -1,12 +1,41 @@ import { renderWithProviders } from "@/tests/render"; import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { type FC } from "react"; +import { useLocation } from "react-router"; import { describe, expect, it } from "vitest"; import PublicationsHeader from "./PublicationsHeader"; +const LocationProbe: FC = () => { + const location = useLocation(); + + return ( +
{`${location.pathname}${location.search}`}
+ ); +}; + describe("PublicationsHeader", () => { it("renders search input", () => { renderWithProviders(); expect(screen.getByRole("searchbox")).toBeInTheDocument(); }); + + it("updates the query param when search is submitted", async () => { + const user = userEvent.setup(); + + renderWithProviders( + <> + + + , + ); + + await user.type(screen.getByRole("searchbox"), "jammy"); + await user.keyboard("{Enter}"); + + expect(screen.getByTestId("location-probe")).toHaveTextContent( + "query=jammy", + ); + }); }); diff --git a/src/tests/mocks/mirrors.ts b/src/tests/mocks/mirrors.ts index da78512d6b..23e4e6ab74 100644 --- a/src/tests/mocks/mirrors.ts +++ b/src/tests/mocks/mirrors.ts @@ -47,4 +47,32 @@ export const mirrors = [ fingerprint: "ABCDEF1234567890", }, }, + { + name: "mirrors/ubuntu-snapshots-mirror", + mirrorId: "ubuntu-snapshots-mirror", + displayName: "Ubuntu snapshots mirror", + archiveRoot: "https://snapshot.ubuntu.com/ubuntu", + distribution: "jammy", + components: ["main"], + architectures: ["amd64"], + preserveSignatures: false, + downloadInstaller: false, + downloadSources: false, + downloadUdebs: false, + lastDownloadDate: new Date("2024-03-01T12:00:00Z"), + }, + { + name: "mirrors/ubuntu-pro-mirror", + mirrorId: "ubuntu-pro-mirror", + displayName: "Ubuntu Pro mirror", + archiveRoot: "https://esm.ubuntu.com/ubuntu", + distribution: "jammy", + components: ["main"], + architectures: ["amd64"], + preserveSignatures: false, + downloadInstaller: false, + downloadSources: false, + downloadUdebs: false, + lastDownloadDate: new Date("2024-02-01T12:00:00Z"), + }, ] as const satisfies Mirror[]; diff --git a/src/tests/server/handlers/mirrors.ts b/src/tests/server/handlers/mirrors.ts index 33f41a0282..a1e53eb61a 100644 --- a/src/tests/server/handlers/mirrors.ts +++ b/src/tests/server/handlers/mirrors.ts @@ -22,6 +22,11 @@ import { const mirrors = [...(mockMirrors as Mirror[])]; +export const resetMirrors = (): void => { + mirrors.length = 0; + mirrors.push(...(mockMirrors as Mirror[])); +}; + const getMirrorsResponse = (requestUrl: string) => { const { pageSize, pageToken } = getDebArchivePaginationParams(requestUrl); const { paginatedData, nextPageToken } = getDebArchivePaginatedResponse( @@ -108,6 +113,23 @@ export default [ `${API_URL_DEB_ARCHIVE}mirrors/:mirrorId/packages`, async ({ params }) => { await delay(); + const endpointStatus = getEndpointStatus(); + + if ( + endpointStatus.status === "error" && + endpointStatus.path === "mirrors/:mirrorId/packages" + ) { + return ENDPOINT_STATUS_API_ERROR; + } + + if ( + endpointStatus.status === "empty" && + endpointStatus.path === "mirrors/:mirrorId/packages" + ) { + return HttpResponse.json({ + mirrorPackages: [], + }); + } const mirror = mirrors.find( ({ mirrorId }) => mirrorId === params.mirrorId, @@ -128,6 +150,15 @@ export default [ async ({ params, request }) => { await delay(); + const endpointStatus = getEndpointStatus(); + + if ( + endpointStatus.status === "error" && + endpointStatus.path === "mirrors/:mirrorId" + ) { + return ENDPOINT_STATUS_API_ERROR; + } + const mirrorIndex = mirrors.findIndex( ({ mirrorId }) => mirrorId === params.mirrorId, ); diff --git a/src/tests/server/handlers/publicationTargets.ts b/src/tests/server/handlers/publicationTargets.ts index d88df9761c..5be2abd144 100644 --- a/src/tests/server/handlers/publicationTargets.ts +++ b/src/tests/server/handlers/publicationTargets.ts @@ -1,6 +1,6 @@ import { API_URL_DEB_ARCHIVE } from "@/constants"; import { getEndpointStatus } from "@/tests/controllers/controller"; -import { publicationTargets } from "@/tests/mocks/publicationTargets"; +import { publicationTargets as _originalPublicationTargets } from "@/tests/mocks/publicationTargets"; import type { PublicationTarget, BatchGetPublicationTargetsResponse, @@ -12,6 +12,15 @@ import { getDebArchivePaginationParams, } from "./_helpers"; +const publicationTargets: PublicationTarget[] = [ + ..._originalPublicationTargets, +]; + +export const resetPublicationTargets = (): void => { + publicationTargets.length = 0; + publicationTargets.push(..._originalPublicationTargets); +}; + const getPublicationTargetsResponse = (requestUrl: string) => { const { pageSize, pageToken } = getDebArchivePaginationParams(requestUrl); const { paginatedData, nextPageToken } = getDebArchivePaginatedResponse( diff --git a/src/tests/setup.ts b/src/tests/setup.ts index aa812fadcf..f607f3173d 100644 --- a/src/tests/setup.ts +++ b/src/tests/setup.ts @@ -10,6 +10,8 @@ import { } from "./helpers"; import "./matcher"; import server from "./server"; +import { resetPublicationTargets } from "./server/handlers/publicationTargets"; +import { resetMirrors } from "./server/handlers/mirrors"; expect.extend(matchers); @@ -99,6 +101,8 @@ afterAll(() => { }); afterEach(() => { + resetPublicationTargets(); + resetMirrors(); setEndpointStatus("default"); server.resetHandlers(); cleanup();