diff --git a/.eslintrc.json b/.eslintrc.json index 212b26a..ee2bd75 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,7 +14,10 @@ "**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ], - "extends": ["plugin:testing-library/react"] + "extends": ["plugin:testing-library/react"], + "rules": { + "testing-library/no-debugging-utils": "off" + } } ], "rules": { diff --git a/src/__mocks__/atobMock.js b/src/__mocks__/atobMock.js index 8a2a691..45245e8 100644 --- a/src/__mocks__/atobMock.js +++ b/src/__mocks__/atobMock.js @@ -1,9 +1,14 @@ const atobMock = () => { window.atob = jest.fn().mockImplementation(str => { - const decoded = Buffer.from(str, "base64").toString("utf-8"); - console.log(`Decoding: ${str}, Result: ${decoded}`); + const base64Pattern = + /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/; - return decoded; + if (!base64Pattern.test(str)) { + // return if string is not base64 encoded + return str; + } + + return Buffer.from(str, "base64").toString("utf-8"); }); }; diff --git a/src/modules/api/context.test.tsx b/src/modules/api/context.test.tsx new file mode 100644 index 0000000..22b47c1 --- /dev/null +++ b/src/modules/api/context.test.tsx @@ -0,0 +1,57 @@ +const originalEnv = process.env; + +beforeEach(() => { + jest.resetModules(); + process.env = { + ...originalEnv, + NEXT_PUBLIC_REKOR_DEFAULT_DOMAIN: "https://example.com", + }; +}); + +afterEach(() => { + process.env = originalEnv; +}); + +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { + RekorClientProvider, + useRekorClient, + useRekorBaseUrl, +} from "./context"; + +const TestConsumerComponent = () => { + useRekorClient(); + const [baseUrl, setBaseUrl] = useRekorBaseUrl(); + + return ( +
+ +

Base URL: {baseUrl}

+
+ ); +}; + +describe("RekorClientContext", () => { + beforeAll(() => jest.clearAllMocks()); + + it("provides a RekorClient instance and manages base URL", async () => { + render( + + + , + ); + + expect( + screen.getByText(/Base URL: https:\/\/example.com/), + ).toBeInTheDocument(); + + await userEvent.click(screen.getByText(/Change Base URL/)); + + expect( + screen.getByText(/Base URL: https:\/\/new.example.com/), + ).toBeInTheDocument(); + }); +}); diff --git a/src/modules/components/DSSE.test.tsx b/src/modules/components/DSSE.test.tsx index 5c7de81..17ac677 100644 --- a/src/modules/components/DSSE.test.tsx +++ b/src/modules/components/DSSE.test.tsx @@ -1,8 +1,13 @@ -jest.mock("next/router"); -// @ts-ignore +// @ts-nocheck import atobMock from "../../__mocks__/atobMock"; +import decodex509Mock from "../../__mocks__/decodex509Mock"; + +jest.mock("next/router"); + +jest.mock("../x509/decode", () => ({ + decodex509: decodex509Mock, +})); -import { RekorClientProvider } from "../api/context"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; import { DSSEViewer } from "./DSSE"; @@ -10,6 +15,7 @@ import { DSSEV001Schema } from "rekor"; describe("DSSEViewer Component", () => { beforeAll(() => { + jest.clearAllMocks(); atobMock(); }); @@ -32,11 +38,7 @@ describe("DSSEViewer Component", () => { }; it("renders without crashing", () => { - render( - - - , - ); + render(); expect(screen.getByText("Hash")).toBeInTheDocument(); }); @@ -56,7 +58,7 @@ describe("DSSEViewer Component", () => { ).toBeInTheDocument(); }); - it.skip("displays the public key certificate title and content correctly", () => { + it("displays the public key certificate title and content correctly", () => { render(); expect(screen.getByText("Public Key Certificate")).toBeInTheDocument(); }); diff --git a/src/modules/components/Entry.test.tsx b/src/modules/components/Entry.test.tsx index 25053d7..d510a7e 100644 --- a/src/modules/components/Entry.test.tsx +++ b/src/modules/components/Entry.test.tsx @@ -1,28 +1,51 @@ +// @ts-nocheck jest.mock("react-syntax-highlighter/dist/cjs/styles/prism", () => ({})); jest.mock("../utils/date", () => ({ toRelativeDateString: jest.fn().mockReturnValue("Some Date"), })); +jest.mock("./HashedRekord", () => ({ + HashedRekordViewer: () =>
MockedHashedRekordViewer
, +})); + +import atobMock from "../../__mocks__/atobMock"; import { fireEvent, render, screen } from "@testing-library/react"; import { Entry, EntryCard } from "./Entry"; -const mockEntry = { - someUuid: { - body: Buffer.from( - JSON.stringify({ kind: "hashedrekord", apiVersion: "v1", spec: {} }), - ).toString("base64"), - attestation: { data: Buffer.from("{}").toString("base64") }, - logID: "123", - logIndex: 123, - integratedTime: 1618886400, - publicKey: "mockedPublicKey", - }, -}; - describe("Entry", () => { - it.skip("renders and toggles the accordion content", () => { + beforeAll(() => { + atobMock(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + const mockEntry = { + someUuid: { + body: Buffer.from( + JSON.stringify({ kind: "hashedrekord", apiVersion: "v1", spec: {} }), + ).toString("base64"), + attestation: { data: Buffer.from("{}").toString("base64") }, + logID: "123", + logIndex: 123, + integratedTime: 1618886400, + publicKey: "mockedPublicKey", + signature: { + publicKey: { + content: window.btoa( + "-----BEGIN CERTIFICATE-----certContent-----END CERTIFICATE-----", + ), // base64 encode + }, + }, + }, + }; + + it("renders and toggles the accordion content", () => { render(); + expect(screen.getByText("apiVersion")).not.toBeVisible(); + // check if UUID link is rendered expect(screen.getByText("someUuid")).toBeInTheDocument(); @@ -31,9 +54,7 @@ describe("Entry", () => { fireEvent.click(toggleButton); // now the accordion content should be visible - expect( - screen.getByText("Your expected content after decoding and dumping"), - ).toBeInTheDocument(); + expect(screen.getByText("apiVersion")).toBeVisible(); }); }); diff --git a/src/modules/components/Explorer.test.tsx b/src/modules/components/Explorer.test.tsx index 8a6f994..bf338a9 100644 --- a/src/modules/components/Explorer.test.tsx +++ b/src/modules/components/Explorer.test.tsx @@ -1,26 +1,58 @@ -jest.mock("next/router"); +import { NextRouter, useRouter } from "next/router"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +jest.mock("next/router", () => ({ + useRouter: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); + + (useRouter as jest.Mock).mockImplementation( + (): Partial => ({ + query: {}, + pathname: "/", + asPath: "/", + }), + ); +}); + +import { render, screen, waitFor } from "@testing-library/react"; import { RekorClientProvider } from "../api/context"; -import { Explorer } from "./Explorer"; +import { Explorer, RekorError } from "./Explorer"; +import userEvent from "@testing-library/user-event"; describe("Explorer", () => { - jest.mock("../api/rekor_api", () => ({ - useRekorSearch: jest.fn(() => - jest.fn().mockImplementation(() => { - return Promise.resolve({ entries: [], totalCount: 0 }); - }), - ), - })); + it("should render search form and display search button", () => { + render( + + + , + ); + + expect(screen.getByLabelText("Attribute")).toBeInTheDocument(); + expect(screen.getByLabelText("Email")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Search" })).toBeInTheDocument(); + }); + + it("should handle invalid logIndex query parameter", () => { + const mockRouter = { + query: { + logIndex: "invalid", + }, + push: jest.fn(), + }; + + (useRouter as jest.Mock).mockImplementation( + (): Partial => mockRouter, + ); - it("renders without issues", () => { render( , ); - expect(screen.getByText("Search")).toBeInTheDocument(); + expect(mockRouter.push).not.toHaveBeenCalled(); }); it("displays loading indicator when fetching data", async () => { @@ -31,16 +63,32 @@ describe("Explorer", () => { ); const button = screen.getByText("Search"); - fireEvent.click(button); + await userEvent.click(button); await waitFor(() => expect(screen.queryByRole("status")).toBeNull()); expect( - screen - .findByLabelText("Showing" || "No matching entries found") - .then(res => { - expect(res).toBeInTheDocument(); - }), + screen.findByLabelText("Showing").then(res => { + screen.debug(); + console.log(res); + expect(res).toBeInTheDocument(); + }), ); }); + + describe("RekorError", () => { + it("should render an Alert component if the error parameter is undefined", () => { + render(); + const alert = screen.getByRole("alert"); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveTextContent("Unknown error"); + }); + + it("should render an Alert component if error parameter is an empty object", () => { + render(); + const alert = screen.getByRole("alert"); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveTextContent("Unknown error"); + }); + }); }); diff --git a/src/modules/components/Explorer.tsx b/src/modules/components/Explorer.tsx index 5495301..ddb5c07 100644 --- a/src/modules/components/Explorer.tsx +++ b/src/modules/components/Explorer.tsx @@ -26,7 +26,7 @@ function isRekorError(error: unknown): error is RekorError { return !!error && typeof error === "object"; } -function Error({ error }: { error: unknown }) { +export function RekorError({ error }: { error: unknown }) { let title = "Unknown error"; let detail: string | undefined; @@ -47,6 +47,7 @@ function Error({ error }: { error: unknown }) { style={{ margin: "1em auto" }} title={title} variant={"danger"} + role={"alert"} > {detail} @@ -181,7 +182,7 @@ export function Explorer() { /> {error ? ( - + ) : loading ? ( ) : ( diff --git a/src/modules/components/HashedRekord.test.tsx b/src/modules/components/HashedRekord.test.tsx index 44f2fca..2f124d6 100644 --- a/src/modules/components/HashedRekord.test.tsx +++ b/src/modules/components/HashedRekord.test.tsx @@ -1,6 +1,13 @@ +// @ts-nocheck jest.mock("next/router"); jest.mock("react-syntax-highlighter/dist/cjs/styles/prism"); +import decodex509Mock from "../../__mocks__/decodex509Mock"; + +jest.mock("../x509/decode", () => ({ + decodex509: decodex509Mock, +})); + import { HashedRekordViewer } from "./HashedRekord"; import { render, screen } from "@testing-library/react"; import { HashedRekorV001Schema } from "rekor"; @@ -30,7 +37,7 @@ describe("HashedRekordViewer", () => { expect(screen.getByText("mockedPublicKeyContent")).toBeInTheDocument(); }); - it.skip("renders the component with a public key certificate", () => { + it("renders the component with a public key certificate", () => { const mockedRekordWithCert = { // simulate a certificate data: {}, @@ -45,7 +52,10 @@ describe("HashedRekordViewer", () => { render(); - // verify that the decoded certificate content is displayed - expect(screen.getByText(/Decoded:/)).toBeInTheDocument(); + expect( + screen.getByText( + /'-----BEGIN CERTIFICATE-----Mocked Certificate-----END CERTIFICATE-----'/, + ), + ).toBeInTheDocument(); }); }); diff --git a/src/modules/components/Intoto.test.tsx b/src/modules/components/Intoto.test.tsx index 1350989..2dd60e7 100644 --- a/src/modules/components/Intoto.test.tsx +++ b/src/modules/components/Intoto.test.tsx @@ -39,7 +39,7 @@ describe("IntotoViewer", () => { }, }; - it.skip("renders the component with payload hash, signature, and certificate", () => { + it("renders the component with payload hash, signature, and certificate", () => { render(); // verify the hash link is rendered correctly diff --git a/src/modules/x509/decode.test.ts b/src/modules/x509/decode.test.ts new file mode 100644 index 0000000..47d934f --- /dev/null +++ b/src/modules/x509/decode.test.ts @@ -0,0 +1,49 @@ +jest.mock("./constants", () => ({ + digitalSignature: "digitalSignature", + nonRepudiation: "nonRepudiation", + keyEncipherment: "keyEncipherment", + dataEncipherment: "dataEncipherment", +})); + +jest.mock("@peculiar/x509", () => ({ + X509Certificate: jest.fn().mockImplementation(() => ({ + extensions: [], + serialNumber: "123", + issuer: { organization: ["Test Org"] }, + notBefore: new Date("2020-01-01"), + notAfter: new Date("2025-01-01"), + publicKey: { algorithm: "rsaEncryption" }, + subjectName: "CN=Test", + })), +})); + +jest.mock("../utils/date", () => ({ + toRelativeDateString: jest.fn(date => date.toISOString()), +})); + +import { decodex509 } from "./decode"; + +describe("decodex509", () => { + it("decodes a raw certificate string", () => { + const rawCertificate = + "-----BEGIN CERTIFICATE-----Mocked Certificate-----END CERTIFICATE-----"; + + const decodedCert = decodex509(rawCertificate); + + expect(decodedCert).toMatchObject({ + data: { + "Serial Number": expect.any(String), + }, + Signature: { + Issuer: expect.any(Object), + Validity: { + "Not Before": expect.any(String), + "Not After": expect.any(String), + }, + Algorithm: expect.any(String), + Subject: expect.any(String), + }, + "X509v3 extensions": expect.any(Object), + }); + }); +});