diff --git a/govtool/frontend/src/utils/dRep.ts b/govtool/frontend/src/utils/dRep.ts index 2135677a1..7743a017e 100644 --- a/govtool/frontend/src/utils/dRep.ts +++ b/govtool/frontend/src/utils/dRep.ts @@ -1,4 +1,4 @@ -import { DRepData } from '@/models'; +import { DRepData } from "@/models"; export const isSameDRep = ( { drepId, view }: DRepData, diff --git a/govtool/frontend/src/utils/tests/canonizeJSON.test.ts b/govtool/frontend/src/utils/tests/canonizeJSON.test.ts new file mode 100644 index 000000000..7115cc430 --- /dev/null +++ b/govtool/frontend/src/utils/tests/canonizeJSON.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import { canonizeJSON } from ".."; + +const exampleJson = { + "@context": { + "@language": "en-us", + CIP100: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + CIP108: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", + hashAlgorithm: "CIP100:hashAlgorithm", + body: { + "@id": "CIP108:body", + "@context": { + references: { + "@id": "CIP108:references", + "@container": "@set", + "@context": { + GovernanceMetadata: "CIP100:GovernanceMetadataReference", + Other: "CIP100:OtherReference", + label: "CIP100:reference-label", + uri: "CIP100:reference-uri", + referenceHash: { + "@id": "CIP108:referenceHash", + "@context": { + hashDigest: "CIP108:hashDigest", + hashAlgorithm: "CIP100:hashAlgorithm", + }, + }, + }, + }, + title: "CIP108:title", + abstract: "CIP108:abstract", + motivation: "CIP108:motivation", + rationale: "CIP108:rationale", + }, + }, + authors: { + "@id": "CIP100:authors", + "@container": "@set", + "@context": { + name: "http://xmlns.com/foaf/0.1/name", + witness: { + "@id": "CIP100:witness", + "@context": { + witnessAlgorithm: "CIP100:witnessAlgorithm", + publicKey: "CIP100:publicKey", + signature: "CIP100:signature", + }, + }, + }, + }, + }, + authors: [], + hashAlgorithm: { + "@value": "blake2b-256", + }, + body: { + abstract: { + "@value": "Test abstract", + }, + motivation: { + "@value": "Test motivation", + }, + rationale: { + "@value": "Test rationale", + }, + references: [ + { + "@type": "Other", + "CIP108:reference-label": { + "@value": "Label", + }, + "CIP108:reference-uri": { + "@value": "https://www.google.com/", + }, + }, + ], + title: { + "@value": "Test title", + }, + }, +}; + +const expectedOutput = ` +_:c14n0 "blake2b-256" . +_:c14n0 _:c14n2 . +_:c14n1 . +_:c14n1 "Label" . +_:c14n1 "https://www.google.com/" . +_:c14n2 "Test abstract" . +_:c14n2 "Test motivation" . +_:c14n2 "Test rationale" . +_:c14n2 _:c14n1 . +_:c14n2 "Test title" . +` + .trim() + .replace(/\s+\n/g, "\n"); + +describe("canonizeJSON", () => { + it("should correctly canonize a jsonld object to the expected output", async () => { + const result = await canonizeJSON(exampleJson); + const normalizedResult = result.trim().replace(/\s+\n/g, "\n"); + expect(normalizedResult).toBe(expectedOutput); + }); +}); diff --git a/govtool/frontend/src/utils/tests/checkIsMaintenanceOn.test.ts b/govtool/frontend/src/utils/tests/checkIsMaintenanceOn.test.ts new file mode 100644 index 000000000..93aff7b68 --- /dev/null +++ b/govtool/frontend/src/utils/tests/checkIsMaintenanceOn.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import axios from "axios"; +import { checkIsMaintenanceOn } from ".."; + +vi.stubGlobal("location", { + ...window.location, + reload: vi.fn(), +}); + +const axiosGetSpy = vi.spyOn(axios, "get"); + +describe("checkIsMaintenanceOn function", () => { + afterEach(() => { + axiosGetSpy.mockClear(); + vi.resetAllMocks(); + }); + + it("does nothing in development mode", async () => { + vi.stubEnv("VITE_IS_DEV", "true"); + await checkIsMaintenanceOn(); + expect(axiosGetSpy).not.toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("reloads the page if maintenance mode is active", async () => { + vi.stubEnv("VITE_IS_DEV", ""); + axiosGetSpy.mockResolvedValue({ data: true }); + await checkIsMaintenanceOn(); + expect(window.location.reload).toHaveBeenCalled(); + }); + + it("does not reload the page if maintenance mode is not active", async () => { + vi.stubEnv("VITE_IS_DEV", ""); + axiosGetSpy.mockResolvedValue({ data: false }); + await checkIsMaintenanceOn(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it("throws an error if the request fails", async () => { + vi.stubEnv("VITE_IS_DEV", ""); + axiosGetSpy.mockRejectedValue(new Error("Network Error")); + await expect(checkIsMaintenanceOn()).rejects.toThrow( + "Action canceled due to maintenance mode.", + ); + }); +}); diff --git a/govtool/frontend/src/utils/tests/checkIsWalletConnected.test.ts b/govtool/frontend/src/utils/tests/checkIsWalletConnected.test.ts new file mode 100644 index 000000000..54bb44a4f --- /dev/null +++ b/govtool/frontend/src/utils/tests/checkIsWalletConnected.test.ts @@ -0,0 +1,25 @@ +import { checkIsWalletConnected } from ".."; +import { + WALLET_LS_KEY, + setItemToLocalStorage, + removeItemFromLocalStorage, +} from "@/utils/localStorage"; + +describe("checkIsWalletConnected function", () => { + it("returns false when wallet information is present in local storage", () => { + setItemToLocalStorage(`${WALLET_LS_KEY}_name`, "Nami"); + setItemToLocalStorage(`${WALLET_LS_KEY}_stake_key`, "teststakekey"); + const isConnected = checkIsWalletConnected(); + + expect(isConnected).toBe(false); + }); + + it("returns true when wallet information is missing in local storage", () => { + removeItemFromLocalStorage(`${WALLET_LS_KEY}_name`); + removeItemFromLocalStorage(`${WALLET_LS_KEY}_stake_key`); + + const isConnected = checkIsWalletConnected(); + + expect(isConnected).toBe(true); + }); +}); diff --git a/govtool/frontend/src/utils/tests/dRep.test.ts b/govtool/frontend/src/utils/tests/dRep.test.ts new file mode 100644 index 000000000..26558791d --- /dev/null +++ b/govtool/frontend/src/utils/tests/dRep.test.ts @@ -0,0 +1,38 @@ +import { isSameDRep } from ".."; + +import { DRepStatus } from "@/models"; + +type TDRepType = "DRep" | "SoleVoter"; + +const EXAMPLE_DREP = { + drepId: "drep123", + view: "view123", + url: "url", + metadataHash: "hash", + deposit: 10000, + votingPower: 10000, + status: DRepStatus.Active, + type: "DRep" as TDRepType, +}; + +describe("isSameDRep function", () => { + it("returns false if dRepIdOrView is undefined", () => { + const dRepIdOrView = undefined; + expect(isSameDRep(EXAMPLE_DREP, dRepIdOrView)).toBe(false); + }); + + it("returns true if drepId matches dRepIdOrView", () => { + const dRepIdOrView = "drep123"; + expect(isSameDRep(EXAMPLE_DREP, dRepIdOrView)).toBe(true); + }); + + it("returns true if view matches dRepIdOrView", () => { + const dRepIdOrView = "view123"; + expect(isSameDRep(EXAMPLE_DREP, dRepIdOrView)).toBe(true); + }); + + it("returns false if neither drepId nor view matches dRepIdOrView", () => { + const dRepIdOrView = "otherId"; + expect(isSameDRep(EXAMPLE_DREP, dRepIdOrView)).toBe(false); + }); +}); diff --git a/govtool/frontend/src/utils/tests/generateAnchor.test.ts b/govtool/frontend/src/utils/tests/generateAnchor.test.ts new file mode 100644 index 000000000..a0f563112 --- /dev/null +++ b/govtool/frontend/src/utils/tests/generateAnchor.test.ts @@ -0,0 +1,28 @@ +import { vi } from "vitest"; +import { + Anchor, + AnchorDataHash, + URL, +} from "@emurgo/cardano-serialization-lib-asmjs"; +import { generateAnchor } from ".."; + +describe("generateAnchor function", () => { + it("generates an anchor with the provided URL and hash", () => { + const url = "https://example.com"; + const hash = "aabbccddeeff"; + + URL.new = vi.fn().mockReturnValueOnce({}); + AnchorDataHash.from_hex = vi.fn().mockReturnValueOnce({}); + Anchor.new = vi.fn().mockReturnValueOnce({}); + + const spyForAnchor = vi.spyOn(Anchor, "new").mockReturnValue(new Anchor()); + const anchor = generateAnchor(url, hash); + + expect(URL.new).toHaveBeenCalledWith(url); + expect(AnchorDataHash.from_hex).toHaveBeenCalledWith(hash); + expect(spyForAnchor).toHaveBeenCalledWith({}, {}); + expect(anchor).toBeInstanceOf(Anchor); + + spyForAnchor.mockRestore(); + }); +}); diff --git a/govtool/frontend/src/utils/tests/getDRepID.test.ts b/govtool/frontend/src/utils/tests/getDRepID.test.ts new file mode 100644 index 000000000..61bc5470c --- /dev/null +++ b/govtool/frontend/src/utils/tests/getDRepID.test.ts @@ -0,0 +1,57 @@ +import { vi } from "vitest"; +import { formHexToBech32, getPubDRepID } from "../getDRepID"; +import { CardanoApiWallet } from "@/models"; + +const dRepIdHex = "99f2c9a961ff53099796643a514a0640379b706ad310bc751c2997c9"; +const dRepIdBech32 = "drep1n8evn2tplafsn9ukvsa9zjsxgqmekur26vgtcagu9xtujzv2yv8"; + +describe("formHexToBech32 function", () => { + it("returns correct dRep bech32 format", () => { + const bech32Format = formHexToBech32(dRepIdHex); + expect(bech32Format).toBe(dRepIdBech32); + }); + + it("expected undefined when no argument", () => { + const bech32Format = formHexToBech32(); + expect(bech32Format).toBe(undefined); + }); +}); + +const mockGetPubDRepKey = vi.fn(); + +const mockWalletApi = { + cip95: { + getPubDRepKey: mockGetPubDRepKey, + }, +} as unknown as CardanoApiWallet; + +describe("getPubDRepID function", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns the dRepKey, dRepID, and dRepIDBech32 when walletApi returns a valid response", async () => { + const dRepKey = "dRepKey123"; + mockGetPubDRepKey.mockResolvedValueOnce(dRepKey); + const result = await getPubDRepID(mockWalletApi); + expect(result).toEqual({ + dRepKey, + dRepID: expect.any(String), + dRepIDBech32: expect.any(String), + }); + expect(mockGetPubDRepKey).toHaveBeenCalled(); + }); + + it("returns undefined values for dRepKey, dRepID, and dRepIDBech32 when walletApi throws an error", async () => { + mockGetPubDRepKey.mockRejectedValueOnce( + new Error("Failed to get PubDRepKey"), + ); + const result = await getPubDRepID(mockWalletApi); + expect(result).toEqual({ + dRepKey: undefined, + dRepID: undefined, + dRepIDBech32: undefined, + }); + expect(mockGetPubDRepKey).toHaveBeenCalled(); + }); +}); diff --git a/govtool/frontend/src/utils/tests/localStorage.test.ts b/govtool/frontend/src/utils/tests/localStorage.test.ts new file mode 100644 index 000000000..8cf7bc8b3 --- /dev/null +++ b/govtool/frontend/src/utils/tests/localStorage.test.ts @@ -0,0 +1,27 @@ +import { + getItemFromLocalStorage, + setItemToLocalStorage, + removeItemFromLocalStorage, +} from ".."; + +const EXAMPLE_KEY = "example_key"; +const VALUE = "exampleValue"; + +describe("localStorage util", () => { + it("returns correctly value after set item to localstorage", () => { + setItemToLocalStorage(EXAMPLE_KEY, VALUE); + + const itemFromStorage = getItemFromLocalStorage(EXAMPLE_KEY); + + expect(itemFromStorage).toBe(VALUE); + }); + + it("returns null after remove item from localstorage", () => { + setItemToLocalStorage(EXAMPLE_KEY, VALUE); + removeItemFromLocalStorage(EXAMPLE_KEY); + + const itemFromStorage = getItemFromLocalStorage(EXAMPLE_KEY); + + expect(itemFromStorage).toBe(null); + }); +}); diff --git a/govtool/frontend/src/utils/tests/numberValidation.test.ts b/govtool/frontend/src/utils/tests/numberValidation.test.ts new file mode 100644 index 000000000..4a7e332ec --- /dev/null +++ b/govtool/frontend/src/utils/tests/numberValidation.test.ts @@ -0,0 +1,36 @@ +import i18n from "@/i18n"; +import { numberValidation } from ".."; + +const positiveResponse = i18n.t( + "createGovernanceAction.fields.validations.positive", +); + +const numberResponse = i18n.t( + "createGovernanceAction.fields.validations.number", +); + +describe("numberValidation function", () => { + it("returns an error message when the input is not a valid number", () => { + const invalidInputs = ["abc", "1.2.3", "10,000.50abc", "/"]; + + invalidInputs.forEach((input) => { + expect(numberValidation(input)).toEqual(numberResponse); + }); + }); + + it("returns an error message when the input is negative", () => { + const negativeInputs = ["-10", "-1.5", "-5000"]; + + negativeInputs.forEach((input) => { + expect(numberValidation(input)).toEqual(positiveResponse); + }); + }); + + it("returns true when the input is a valid positive number", () => { + const validInputs = ["10", "1.5", "5000", "10,5"]; + + validInputs.forEach((input) => { + expect(numberValidation(input)).toEqual(true); + }); + }); +}); diff --git a/govtool/frontend/src/utils/tests/openInNewTab.test.ts b/govtool/frontend/src/utils/tests/openInNewTab.test.ts new file mode 100644 index 000000000..ebecfa62c --- /dev/null +++ b/govtool/frontend/src/utils/tests/openInNewTab.test.ts @@ -0,0 +1,33 @@ +import { vi } from "vitest"; +import { openInNewTab } from ".."; + +describe("openInNewTab function", () => { + it("opens a new tab with the provided URL", () => { + const originalOpen = window.open; + const mockOpen = vi.fn(); + window.open = mockOpen; + + const url = "https://example.com"; + openInNewTab(url); + + expect(mockOpen).toHaveBeenCalledWith(url, "_blank", "noopener,noreferrer"); + + window.open = originalOpen; + }); + + it("sets opener to null if new window is opened", () => { + const originalOpen = window.open; + const mockNewWindow = { + opener: "someOpener", + }; + const mockOpen = vi.fn().mockReturnValue(mockNewWindow); + window.open = mockOpen; + + const url = "https://example.com"; + openInNewTab(url); + + expect(mockNewWindow.opener).toBeNull(); + + window.open = originalOpen; + }); +}); diff --git a/govtool/frontend/src/utils/tests/validateMetadataHash.test.ts b/govtool/frontend/src/utils/tests/validateMetadataHash.test.ts new file mode 100644 index 000000000..d04f8d2fc --- /dev/null +++ b/govtool/frontend/src/utils/tests/validateMetadataHash.test.ts @@ -0,0 +1,75 @@ +import { vi } from "vitest"; +import { postValidate } from "@services"; +import { checkIsMissingGAMetadata } from ".."; +import { MetadataStandard, MetadataValidationStatus } from "@/models"; + +const url = "https://example.com"; +const hash = "abcdefg"; + +vi.mock("@services"); + +const mockPostValidate = postValidate as jest.MockedFunction< + typeof postValidate +>; + +describe("checkIsMissingGAMetadata", () => { + it("returns false when there are no issues with the metadata", async () => { + mockPostValidate.mockResolvedValueOnce({ + valid: true, + }); + + const result = await checkIsMissingGAMetadata({ url, hash }); + + expect(result).toBe(false); + expect(mockPostValidate).toHaveBeenCalledWith({ + url, + hash, + standard: MetadataStandard.CIP108, + }); + }); + + it("returns MetadataValidationStatus.INVALID_HASH when postValidate resolves with INVALID_HASH", async () => { + mockPostValidate.mockResolvedValueOnce({ + valid: false, + status: MetadataValidationStatus.INVALID_HASH, + }); + + const result = await checkIsMissingGAMetadata({ url, hash }); + + expect(result).toBe(MetadataValidationStatus.INVALID_HASH); + expect(mockPostValidate).toHaveBeenCalledWith({ + url, + hash, + standard: MetadataStandard.CIP108, + }); + }); + + it("returns MetadataValidationStatus.INVALID_JSONLD when postValidate resolves with INVALID_JSONLD", async () => { + mockPostValidate.mockResolvedValueOnce({ + valid: false, + status: MetadataValidationStatus.INVALID_JSONLD, + }); + + const result = await checkIsMissingGAMetadata({ url, hash }); + + expect(result).toBe(MetadataValidationStatus.INVALID_JSONLD); + expect(mockPostValidate).toHaveBeenCalledWith({ + url, + hash, + standard: MetadataStandard.CIP108, + }); + }); + + it("returns MetadataValidationStatus.URL_NOT_FOUND when postValidate throws an error", async () => { + mockPostValidate.mockRejectedValueOnce(new Error("404 Not Found")); + + const result = await checkIsMissingGAMetadata({ url, hash }); + + expect(result).toBe(MetadataValidationStatus.URL_NOT_FOUND); + expect(mockPostValidate).toHaveBeenCalledWith({ + url, + hash, + standard: MetadataStandard.CIP108, + }); + }); +}); diff --git a/govtool/frontend/src/utils/tests/wait.test.ts b/govtool/frontend/src/utils/tests/wait.test.ts new file mode 100644 index 000000000..1949f11c1 --- /dev/null +++ b/govtool/frontend/src/utils/tests/wait.test.ts @@ -0,0 +1,28 @@ +import { wait } from ".."; + +describe("wait function", () => { + it("resolves after the specified time", async () => { + const startTime = Date.now(); + const waitTime = 2000; + + await wait(waitTime); + + const endTime = Date.now(); + const elapsedTime = endTime - startTime; + + expect(elapsedTime).toBeGreaterThanOrEqual(waitTime - 100); + expect(elapsedTime).toBeLessThanOrEqual(waitTime + 100); + }); + + it("resolves after the default time if no time is specified", async () => { + const startTime = Date.now(); + + await wait(); + + const endTime = Date.now(); + const elapsedTime = endTime - startTime; + + expect(elapsedTime).toBeGreaterThanOrEqual(4900); + expect(elapsedTime).toBeLessThanOrEqual(5100); + }); +});