Skip to content

Commit

Permalink
chore: clean up and handle no template case
Browse files Browse the repository at this point in the history
  • Loading branch information
yapyuyou committed Mar 21, 2024
1 parent 04291a7 commit 5c8fc53
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 61 deletions.
74 changes: 47 additions & 27 deletions src/components/renderer/SvgRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
v4WithTamperedEmbeddedSvgAndDigestMultibase,
v2WithSvgUrlAndDigestMultibase,
v4WithOnlyTamperedEmbeddedSvg,
v4WithNoRenderMethod,
} from "./fixtures/svgRendererSamples";

// const yes = mockResponse.blob().then((res) => {
Expand All @@ -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);
Expand All @@ -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<HTMLIFrameElement>();
const svgUrl = v4WithEmbeddedSvgAndDigestMultibase.renderMethod.id;

const { findByTitle } = render(
<SvgRenderer svgData={svgUrl} document={v4WithEmbeddedSvgAndDigestMultibase} svgRef={svgRef} />
);
const { findByTitle } = render(<SvgRenderer document={v4WithEmbeddedSvgAndDigestMultibase} svgRef={svgRef} />);

const iFrame = await findByTitle("Svg Renderer Frame");
const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc;
Expand Down Expand Up @@ -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<HTMLIFrameElement>();
const svgUrl = v4WithSvgUrlAndDigestMultibase.renderMethod.id;

const { findByTestId } = render(
<SvgRenderer svgData={svgUrl} document={v4WithSvgUrlAndDigestMultibase} svgRef={svgRef} />
const { findByTitle } = render(
<SvgRenderer document={v4WithTamperedEmbeddedSvgAndDigestMultibase} svgRef={svgRef} />
);

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<HTMLIFrameElement>();
const svgUrl = v4WithTamperedEmbeddedSvgAndDigestMultibase.renderMethod.id;

const { findByTestId } = render(
<SvgRenderer svgData={svgUrl} document={v4WithTamperedEmbeddedSvgAndDigestMultibase} svgRef={svgRef} />
);
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 () => {
Expand All @@ -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<HTMLIFrameElement>();

const { findByTestId } = render(<SvgRenderer document={v4WithSvgUrlAndDigestMultibase} svgRef={svgRef} />);

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<HTMLIFrameElement>();

const { findByTestId } = render(<SvgRenderer document={v4WithNoRenderMethod} svgRef={svgRef} />);

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<HTMLIFrameElement>();

const { findByTestId } = render(<SvgRenderer document={v4WithSvgUrlAndDigestMultibase} svgRef={svgRef} />);

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”`);
});
});
57 changes: 23 additions & 34 deletions src/components/renderer/SvgRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -28,71 +28,59 @@ export const SvgRenderer: FunctionComponent<SvgRendererProps> = ({
onConnected,
forceV2 = false,
}) => {
const EMBEDDED_DOCUMENT = "[Embedded SVG]";
const [buffer, setBuffer] = useState<ArrayBuffer>();
const [svgFetchedData, setFetchedSvgData] = useState<string>("");
const [isError, setIsError] = useState<boolean>(false);
const [isFetchError, setIsFetchError] = useState<boolean>(false);
const [source, setSource] = useState<string>("");
let docAsAny: any;
if (forceV2 && utils.isRawV2Document(docAsAny)) {
docAsAny = document as v2.OpenAttestationDocument;
} else {
docAsAny = document as any; // TODO: update type to v4.OpenAttestationDocument
}
if (!("renderMethod" in docAsAny)) {
return <NoTemplate document={docAsAny} handleObfuscation={(value) => value} />;
}

// 1. Fetch svg data from url if needed, if not directly proceed to checksum
// Step 1: Fetch svg data if needed
useEffect(() => {

Check failure on line 47 in src/components/renderer/SvgRenderer.tsx

View workflow job for this annotation

GitHub Actions / Test

React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
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);
Expand All @@ -104,7 +92,7 @@ export const SvgRenderer: FunctionComponent<SvgRendererProps> = ({
}, 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(() => {

Check failure on line 96 in src/components/renderer/SvgRenderer.tsx

View workflow job for this annotation

GitHub Actions / Test

React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
if (!buffer) return;

Expand All @@ -121,14 +109,15 @@ export const SvgRenderer: FunctionComponent<SvgRendererProps> = ({
if (recomputedDigestMultibase === digestMultibaseInDoc) {
setSvgDataAndTriggerCallback(text);
} else {
setIsError(true);
setIsFetchError(true);
}
} else {
setSvgDataAndTriggerCallback(text);
}
/* 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)) {
Expand Down Expand Up @@ -164,7 +153,7 @@ export const SvgRenderer: FunctionComponent<SvgRendererProps> = ({

return (
<>
{isError ? (
{isFetchError ? (
<ConnectionFailureTemplate document={document} source={source} />
) : (
<iframe
Expand Down
20 changes: 20 additions & 0 deletions src/components/renderer/fixtures/svgRendererSamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,23 @@ export const v2WithSvgUrlAndDigestMultibase = {
name: "TAN CHEN CHEN",
},
};

export const v4WithNoRenderMethod = {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json",
],
issuer: {
id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90",
type: "OpenAttestationIssuer",
name: "Government Technology Agency of Singapore (GovTech)",
identityProof: { identityProofType: v4.IdentityProofType.DNSDid, identifier: "example.openattestation.com" },
},
credentialStatus: { type: "OpenAttestationCredentialStatus", credentialStatusType: v4.CredentialStatusType.None },
credentialSubject: {
id: "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42",
type: ["SvgExample"],
course: { name: "SVG Basics Workshop", fromDate: "01/01/2024", endDate: "16/01/2024" },
recipient: { name: "TAN CHEN CHEN" },
},
};

0 comments on commit 5c8fc53

Please sign in to comment.