From 5c8fc53af6ac0cac0bd72526732d4944f3cdb779 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 21 Mar 2024 17:36:22 +0800 Subject: [PATCH] chore: clean up and handle no template case --- src/components/renderer/SvgRenderer.test.tsx | 74 ++++++++++++------- src/components/renderer/SvgRenderer.tsx | 57 ++++++-------- .../renderer/fixtures/svgRendererSamples.ts | 20 +++++ 3 files changed, 90 insertions(+), 61 deletions(-) diff --git a/src/components/renderer/SvgRenderer.test.tsx b/src/components/renderer/SvgRenderer.test.tsx index 1f8d5b3..43ceb19 100644 --- a/src/components/renderer/SvgRenderer.test.tsx +++ b/src/components/renderer/SvgRenderer.test.tsx @@ -25,6 +25,7 @@ import { v4WithTamperedEmbeddedSvgAndDigestMultibase, v2WithSvgUrlAndDigestMultibase, v4WithOnlyTamperedEmbeddedSvg, + v4WithNoRenderMethod, } from "./fixtures/svgRendererSamples"; // const yes = mockResponse.blob().then((res) => { @@ -37,7 +38,7 @@ import { describe("SvgRenderer component", () => { const mockSvg = fs.readFileSync("./src/components/renderer/fixtures/example_cert.svg"); const mockSvgBlob = new Blob([mockSvg], { type: "image/svg+xml" }); - const mockResponse = { blob: () => Promise.resolve(mockSvgBlob) }; + const mockResponse = { ok: true, blob: () => Promise.resolve(mockSvgBlob) }; it("should render v4 doc correctly with a valid SVG URL", async () => { global.fetch = jest.fn().mockResolvedValue(mockResponse); @@ -56,11 +57,8 @@ describe("SvgRenderer component", () => { it("should render v4 doc correctly with a valid embedded SVG", async () => { global.fetch = jest.fn().mockResolvedValue(mockResponse); const svgRef = React.createRef(); - const svgUrl = v4WithEmbeddedSvgAndDigestMultibase.renderMethod.id; - const { findByTitle } = render( - - ); + const { findByTitle } = render(); const iFrame = await findByTitle("Svg Renderer Frame"); const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc; @@ -88,35 +86,23 @@ describe("SvgRenderer component", () => { const tamperedSvgBuffer = Buffer.concat([mockSvg, Buffer.from([0x12, 0x34])]); // Add some random bytes const tamperedSvgBlob = new Blob([tamperedSvgBuffer], { type: "image/svg+xml" }); - const tamperedMockResponse = { blob: () => Promise.resolve(tamperedSvgBlob) }; + const tamperedMockResponse = { ok: true, blob: () => Promise.resolve(tamperedSvgBlob) }; - it("should render default template when SVG at URL has been tampered with", async () => { + it("should render v4 doc with embedded SVG with digestMultibase", async () => { + // If SVG is embedded, digestMultibase will be ignored global.fetch = jest.fn().mockResolvedValue(tamperedMockResponse); const svgRef = React.createRef(); - const svgUrl = v4WithSvgUrlAndDigestMultibase.renderMethod.id; - const { findByTestId } = render( - + const { findByTitle } = render( + ); - const defaultTemplate = await findByTestId("default-template"); - expect(defaultTemplate.textContent).toContain("This document might be having loading issues"); - expect(defaultTemplate.textContent).toContain(`URL: “http://mockbucket.com/static/svg_test.svg”`); - }); - - it("should render default template when embedded SVG has somehow also been tampered with", async () => { - // Leaving this in since users can pre-load and directly pass in the svg data, but we can technically add another check to remove - global.fetch = jest.fn().mockResolvedValue(tamperedMockResponse); - const svgRef = React.createRef(); - const svgUrl = v4WithTamperedEmbeddedSvgAndDigestMultibase.renderMethod.id; - - const { findByTestId } = render( - - ); + const iFrame = await findByTitle("Svg Renderer Frame"); + const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc; - const defaultTemplate = await findByTestId("default-template"); - expect(defaultTemplate.textContent).toContain("This document might be having loading issues"); - // expect(defaultTemplate.textContent).toContain(`URL: “http://mockbucket.com/static/svg_test.svg”`); // TODO: Update default renderer to handle this case + expect(srcdocContent).toContain("SVG document image"); + expect(srcdocContent).toContain(encodeURIComponent("CERTIFICATE OF COMPLETION")); + expect(srcdocContent).toContain(encodeURIComponent("TAN CHEN CHEN")); }); it("should render v4 doc with modified SVG when no digestMultibase", async () => { @@ -135,4 +121,38 @@ describe("SvgRenderer component", () => { expect(srcdocContent).toContain(encodeURIComponent("TAMPERED CERTIFICATE OF COMPLETION")); expect(srcdocContent).toContain(encodeURIComponent("TAN CHEN CHEN")); }); + + it("should render misconfiguration template when SVG at URL has been tampered with", async () => { + global.fetch = jest.fn().mockResolvedValue(tamperedMockResponse); + const svgRef = React.createRef(); + + const { findByTestId } = render(); + + const defaultTemplate = await findByTestId("default-template"); + expect(defaultTemplate.textContent).toContain("This document might be having loading issues"); + expect(defaultTemplate.textContent).toContain(`URL: “http://mockbucket.com/static/svg_test.svg”`); + }); + + it("should render default template when document.RenderMethod is undefined", async () => { + global.fetch = jest.fn().mockResolvedValue(mockResponse); + const svgRef = React.createRef(); + + const { findByTestId } = render(); + + const defaultTemplate = await findByTestId("default-template"); + expect(defaultTemplate.textContent).toContain("The contents of this document have not been formatted"); + expect(defaultTemplate.textContent).toContain("identifier: example.openattestation.com"); + }); + + const badMockResponse = { ok: false }; + it("should render connection error template when SVG cannot be fetched", async () => { + global.fetch = jest.fn().mockResolvedValue(badMockResponse); + const svgRef = React.createRef(); + + const { findByTestId } = render(); + + const defaultTemplate = await findByTestId("default-template"); + expect(defaultTemplate.textContent).toContain("This document might be having loading issues"); + expect(defaultTemplate.textContent).toContain(`URL: “http://mockbucket.com/static/svg_test.svg”`); + }); }); diff --git a/src/components/renderer/SvgRenderer.tsx b/src/components/renderer/SvgRenderer.tsx index ad9dde6..00f37c8 100644 --- a/src/components/renderer/SvgRenderer.tsx +++ b/src/components/renderer/SvgRenderer.tsx @@ -4,7 +4,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { TextEncoder, TextDecoder } from "util"; import crypto from "crypto"; import bs58 from "bs58"; -import { ConnectionFailureTemplate } from "../../DefaultTemplate"; +import { ConnectionFailureTemplate, NoTemplate } from "../../DefaultTemplate"; /* eslint-disable-next-line @typescript-eslint/no-var-requires */ const handlebars = require("handlebars"); @@ -28,9 +28,10 @@ export const SvgRenderer: FunctionComponent = ({ onConnected, forceV2 = false, }) => { + const EMBEDDED_DOCUMENT = "[Embedded SVG]"; const [buffer, setBuffer] = useState(); const [svgFetchedData, setFetchedSvgData] = useState(""); - const [isError, setIsError] = useState(false); + const [isFetchError, setIsFetchError] = useState(false); const [source, setSource] = useState(""); let docAsAny: any; if (forceV2 && utils.isRawV2Document(docAsAny)) { @@ -38,61 +39,48 @@ export const SvgRenderer: FunctionComponent = ({ } else { docAsAny = document as any; // TODO: update type to v4.OpenAttestationDocument } + if (!("renderMethod" in docAsAny)) { + return value} />; + } - // 1. Fetch svg data from url if needed, if not directly proceed to checksum + // Step 1: Fetch svg data if needed useEffect(() => { const svgInDoc = docAsAny.renderMethod.id; const urlPattern = /^(http(s)?:\/\/)?(www\.)?[\w-]+\.[\w]{2,}(\/[\w-]+)*\.svg$/; const isSvgUrl = urlPattern.test(svgInDoc); if (svgData) { + // Case 1: Svg data is pre-fetched and passed as a prop const textEncoder = new TextEncoder(); const svgArrayBuffer = textEncoder.encode(svgData).buffer; setBuffer(svgArrayBuffer); - setSource(isSvgUrl ? svgInDoc : "[Embedded SVG]"); // In case svg data is passed over despite being embedded + setSource(isSvgUrl ? svgInDoc : EMBEDDED_DOCUMENT); // In case svg data is passed over despite being embedded } else if (isSvgUrl) { + // Case 2: Fetch svg data from url in document const fetchSvg = async () => { try { const response = await fetch(svgInDoc); console.log(response); + if (!response.ok) { + throw new Error("Failed to fetch remote SVG"); + } const blob = await response.blob(); console.log(blob); setBuffer(await blob.arrayBuffer()); } catch (error) { console.log(error); - setIsError(true); + setIsFetchError(true); } }; fetchSvg(); - setSource(svgData); + setSource(svgInDoc); } else { - setSvgDataAndTriggerCallback(svgInDoc); // Can directly display if svg is embedded - setSource("[Embedded SVG]"); + // Case 3: Display embedded svg data directly from document + setSvgDataAndTriggerCallback(svgInDoc); + setSource(EMBEDDED_DOCUMENT); } - - // if (urlPattern.test(svgData)) { - // const fetchSvg = async () => { - // try { - // const response = await fetch(svgData); - // console.log(response); - // const blob = await response.blob(); - // console.log(blob); - // setBuffer(await blob.arrayBuffer()); - // } catch (error) { - // console.log(error); - // setIsError(true); - // } - // }; - // fetchSvg(); - // setSource(svgData); - // } else { - // const textEncoder = new TextEncoder(); - // const svgArrayBuffer = textEncoder.encode(svgData).buffer; - // setBuffer(svgArrayBuffer); - // setSource("[Embedded SVG]"); - // } /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [svgData]); + }, [document]); const setSvgDataAndTriggerCallback = (svgToSet: string) => { setFetchedSvgData(svgToSet); @@ -104,7 +92,7 @@ export const SvgRenderer: FunctionComponent = ({ }, 200); // wait for 200ms before manually updating the height }; - // 2. Recompute and compare the digestMultibase if it is in the document, if not proceed to use the svg template + // Step 2: Recompute and compare the digestMultibase if present, if not proceed to use the svg template useEffect(() => { if (!buffer) return; @@ -121,7 +109,7 @@ export const SvgRenderer: FunctionComponent = ({ if (recomputedDigestMultibase === digestMultibaseInDoc) { setSvgDataAndTriggerCallback(text); } else { - setIsError(true); + setIsFetchError(true); } } else { setSvgDataAndTriggerCallback(text); @@ -129,6 +117,7 @@ export const SvgRenderer: FunctionComponent = ({ /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [buffer]); + // Step 3: Compile final svg const renderTemplate = (template: string, document: any, forceV2: boolean) => { if (template.length === 0) return ""; if (forceV2 && utils.isRawV2Document(document)) { @@ -164,7 +153,7 @@ export const SvgRenderer: FunctionComponent = ({ return ( <> - {isError ? ( + {isFetchError ? ( ) : (