Skip to content

Commit

Permalink
test(interface): added tests for userContext, including multichain (p…
Browse files Browse the repository at this point in the history
…assportxyz#1062)

* test(interface): added tests for userContext, including multichain. default to multichain enabled

* test(interface): fixed act() issue with ExpiredModal test
  • Loading branch information
lucianHymer authored Apr 3, 2023
1 parent 0ff2a5f commit 6fb68bd
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 72 deletions.
1 change: 1 addition & 0 deletions app/__test-fixtures__/contextTestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jest.mock("@didtools/cacao", () => ({
export const makeTestUserContext = (initialState?: Partial<UserContextState>): UserContextState => {
return {
loggedIn: true,
loggingIn: false,
toggleConnection: jest.fn(),
handleDisconnection: jest.fn(),
address: mockAddress,
Expand Down
9 changes: 7 additions & 2 deletions app/__tests__/components/ExpiredStampModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { ExpiredStampModal, getProviderIdsFromPlatformId } from "../../components/ExpiredStampModal";
import {
makeTestCeramicContext,
Expand Down Expand Up @@ -61,7 +61,7 @@ describe("ExpiredStampModal", () => {
"FiftyOrMoreGithubFollowers",
]);
});
it("should delete all stamps within each expired platform", () => {
it("should delete all stamps within each expired platform", async () => {
const handleDeleteStamps = jest.fn();
renderWithContext(
{} as UserContextState,
Expand All @@ -70,6 +70,9 @@ describe("ExpiredStampModal", () => {
);

screen.getByTestId("delete-duplicate").click();

const spinner = screen.getByTestId("removing-stamps-spinner");
expect(spinner).toBeInTheDocument();
expect(handleDeleteStamps).toHaveBeenCalledWith([
"Ens",
"Facebook",
Expand All @@ -83,5 +86,7 @@ describe("ExpiredStampModal", () => {
"FiftyOrMoreGithubFollowers",
"Linkedin",
]);

await waitFor(() => expect(spinner).not.toBeInTheDocument());
});
});
131 changes: 86 additions & 45 deletions app/__tests__/context/userContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { render, waitFor, screen, act } from "@testing-library/react";
import { render, waitFor, screen } from "@testing-library/react";
import * as framework from "@self.id/framework";
import { EthereumWebAuth } from "@didtools/pkh-ethereum";
import { AccountId } from "caip";
import { useContext, useEffect, useState } from "react";
import { mockWallet } from "../../__test-fixtures__/onboardHookValues";
import { mockAddress, mockWallet } from "../../__test-fixtures__/onboardHookValues";
import { makeTestCeramicContext } from "../../__test-fixtures__/contextTestHelpers";

import { UserContext, UserContextProvider } from "../../context/userContext";
Expand All @@ -21,6 +23,16 @@ jest.mock("@didtools/pkh-ethereum", () => {
};
});

jest.mock("did-session", () => {
return {
DIDSession: {
authorize: () => ({
serialize: jest.fn(),
}),
},
};
});

jest.mock("@self.id/web", () => {
return {
EthereumAuthProvider: jest.fn(),
Expand All @@ -33,43 +45,21 @@ jest.mock("@self.id/framework", () => {
};
});

const localStorageMock = (function () {
let store: any = {};

return {
getItem(key: any) {
return store[key];
},

setItem(key: any, value: any) {
store[key] = value;
},

clear() {
store = {};
},

removeItem(key: any) {
delete store[key];
},

getAll() {
return store;
},
};
})();

Object.defineProperty(window, "localStorage", { value: localStorageMock });

const TestingComponent = () => {
const { wallet } = useContext(UserContext);
const { loggingIn } = useContext(UserContext);
const [session, setSession] = useState("");

useEffect(() => {
// using https://www.npmjs.com/package/jest-localstorage-mock to mock localStorage
setSession(localStorage.getItem("didsession-0xmyAddress") ?? "");
});

return <div data-testid="session-id">{session}</div>;
return (
<div>
<div data-testid="session-id">{session}</div>
<div>Logging In: {String(loggingIn)}</div>
</div>
);
};

const mockCeramicContext = makeTestCeramicContext({
Expand All @@ -81,31 +71,82 @@ const mockCeramicContext = makeTestCeramicContext({
});

describe("<UserContext>", () => {
it.skip("should delete localStorage item if session has expired", async () => {
const renderTestComponent = () =>
render(
<UserContextProvider>
<CeramicContext.Provider value={mockCeramicContext}>
<TestingComponent />
</CeramicContext.Provider>
</UserContextProvider>
);

beforeEach(() => {
localStorage.setItem("connectedWallets", "[]");
});

it("should delete localStorage item if session has expired", async () => {
const ceramicConnect = jest.fn().mockResolvedValueOnce({
client: {
session: {
isExpired: true,
expireInSecs: 3500,
expireInSecs: 3400,
},
},
});
(framework.useViewerConnection as jest.Mock).mockReturnValue([{ status: "connected" }, ceramicConnect, jest.fn()]);
(framework.useViewerConnection as jest.Mock).mockReturnValue([
{ status: "connecting", selfID: { did: "did:test" } },
ceramicConnect,
jest.fn(),
]);

localStorage.setItem("didsession-0xmyAddress", "eyJzZXNzaW9uS2V5U2VlZCI6IlF5cTN4aW9ubGxD...");

renderTestComponent();

expect(screen.getByTestId("session-id")).toHaveTextContent("eyJzZXNzaW9uS2V5U2VlZCI6IlF5cTN4aW9ubGxD...");

await waitFor(() => expect(screen.getByText("Logging In: false")).toBeInTheDocument());
await waitFor(() => expect(screen.getByTestId("session-id").textContent).toBe(""));
});

describe("when using multichain", () => {
beforeEach(async () => {
const ceramicConnect = jest.fn().mockResolvedValueOnce({
client: {},
});
(framework.useViewerConnection as jest.Mock).mockReturnValue([
{ status: "connecting" },
ceramicConnect,
jest.fn(),
]);
});

it("should use chain id 1 in the DID regardless of the wallet chain", async () => {
renderTestComponent();

await waitFor(() => expect(screen.getByText("Logging In: true")).toBeInTheDocument());

localStorageMock.setItem("didsession-0xmyAddress", "eyJzZXNzaW9uS2V5U2VlZCI6IlF5cTN4aW9ubGxD...");
await waitFor(() => expect(screen.getByText("Logging In: false")).toBeInTheDocument());

act(() => {
render(
<UserContextProvider>
<CeramicContext.Provider value={mockCeramicContext}>
<TestingComponent />
</CeramicContext.Provider>
</UserContextProvider>
expect(EthereumWebAuth.getAuthMethod as jest.Mock).toHaveBeenCalledWith(
mockWallet.provider,
new AccountId({ address: mockAddress, chainId: "eip155:1" })
);
});

expect(screen.getByTestId("session-id")).toHaveTextContent("eyJzZXNzaW9uS2V5U2VlZCI6IlF5cTN4aW9ubGxD...");
it("should create a DID with id 1 when switching to a different chain", async () => {
localStorage.setItem("didsession-0xmyAddress", "eyJzZXNzaW9uS2V5U2VlZCI6IlF5cTN4aW9ubGxD...");

renderTestComponent();

await waitFor(() => expect(screen.getByTestId("session-id")).toHaveTextContent(""));
await waitFor(() => expect(screen.getByText("Logging In: true")).toBeInTheDocument());

await waitFor(() => expect(screen.getByText("Logging In: false")).toBeInTheDocument());

expect(EthereumWebAuth.getAuthMethod as jest.Mock).toHaveBeenCalledWith(
mockWallet.provider,
new AccountId({ address: mockAddress, chainId: "eip155:1" })
);
});
});
});
3 changes: 2 additions & 1 deletion app/components/ExpiredStampModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export const ExpiredStampModal = ({ isOpen, onClose }: ExpiredStampModalProps) =
onClick={() => deleteAndNotify()}
className="sidebar-verify-btn w-1/2"
>
{isRemovingStamps && <Spinner size="sm" className="my-auto mr-2" />} Remove
{isRemovingStamps && <Spinner data-testid="removing-stamps-spinner" size="sm" className="my-auto mr-2" />}{" "}
Remove
</button>
</div>
</div>
Expand Down
28 changes: 9 additions & 19 deletions app/context/userContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import axios from "axios";

export type DbAuthTokenStatus = "idle" | "failed" | "connected" | "connecting";

const MULTICHAIN_ENABLED = process.env.NEXT_PUBLIC_FF_MULTICHAIN_SIGNATURE !== "off";

export interface UserContextState {
loggedIn: boolean;
toggleConnection: () => void;
Expand All @@ -35,6 +37,7 @@ export interface UserContextState {
walletLabel: string | undefined;
dbAccessToken: string | undefined;
dbAccessTokenStatus: DbAuthTokenStatus;
loggingIn: boolean;
}

const startingState: UserContextState = {
Expand All @@ -47,6 +50,7 @@ const startingState: UserContextState = {
walletLabel: undefined,
dbAccessToken: undefined,
dbAccessTokenStatus: "idle",
loggingIn: false,
};

export const pillLocalStorage = (platform?: string): void => {
Expand All @@ -72,7 +76,7 @@ export const UserContextProvider = ({ children }: { children: any }) => {
const [walletLabel, setWalletLabel] = useState<string | undefined>();
const [address, setAddress] = useState<string>();
const [signer, setSigner] = useState<JsonRpcSigner | undefined>();
const [loggingIn, setLoggingIn] = useState<boolean | undefined>();
const [loggingIn, setLoggingIn] = useState<boolean>(false);
const [dbAccessToken, setDbAccessToken] = useState<string | undefined>();
const [dbAccessTokenStatus, setDbAccessTokenStatus] = useState<DbAuthTokenStatus>("idle");

Expand Down Expand Up @@ -179,7 +183,7 @@ export const UserContextProvider = ({ children }: { children: any }) => {
// check that passportLogin isn't mid-way through
if (wallet && !loggingIn) {
// ensure that passport is connected to mainnet
const hasCorrectChainId = process.env.NEXT_PUBLIC_FF_MULTICHAIN_SIGNATURE === "on" ? true : await ensureMainnet();
const hasCorrectChainId = MULTICHAIN_ENABLED ? true : await ensureMainnet();
// mark that we're attempting to login
setLoggingIn(true);
// with loaded chainId
Expand All @@ -197,8 +201,6 @@ export const UserContextProvider = ({ children }: { children: any }) => {
const dbCacheTokenKey = `dbcache-token-${address}`;
const sessionStr = window.localStorage.getItem(sessionKey);

let hasNewSelfId = false;

// @ts-ignore
// When sessionStr is null, this will create a new selfId. We want to avoid this, becasue we want to make sure
// that chainId 1 is in the did
Expand All @@ -210,8 +212,7 @@ export const UserContextProvider = ({ children }: { children: any }) => {
// @ts-ignore
!selfId?.client?.session
) {
hasNewSelfId = true;
if (process.env.NEXT_PUBLIC_FF_MULTICHAIN_SIGNATURE === "on") {
if (MULTICHAIN_ENABLED) {
// If the session loaded is not valid, or if it is expired or close to expire, we create
// a new session
// Also we enforce the "1" chainId, as we always want to use mainnet dids, in order to avoid confusion
Expand Down Expand Up @@ -252,6 +253,7 @@ export const UserContextProvider = ({ children }: { children: any }) => {
});
// then clear local state
clearState();

window.localStorage.removeItem(sessionKey);
window.localStorage.removeItem(dbCacheTokenKey);
}
Expand Down Expand Up @@ -400,19 +402,6 @@ export const UserContextProvider = ({ children }: { children: any }) => {
});
};

const stateMemo = useMemo(
() => ({
loggedIn,
address,
toggleConnection,
wallet,
signer,
walletLabel,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[loggedIn, address, signer, wallet]
);

// use props as a way to pass configuration values
const providerProps = {
loggedIn,
Expand All @@ -424,6 +413,7 @@ export const UserContextProvider = ({ children }: { children: any }) => {
walletLabel,
dbAccessToken,
dbAccessTokenStatus,
loggingIn,
};

return <UserContext.Provider value={providerProps}>{children}</UserContext.Provider>;
Expand Down
2 changes: 2 additions & 0 deletions app/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const customJestConfig = {
"^@/pages/(.*)$": "<rootDir>/pages/$1",
},
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
setupFiles: ["jest-localstorage-mock"],
resetMocks: false,
};

export default createJestConfig(customJestConfig);
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"eslint-plugin-testing-library": "^5.3.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"jest-localstorage-mock": "^2.4.21",
"jest-localstorage-mock": "^2.4.26",
"jest-mock-extended": "^2.0.5",
"postcss": "^8.4.5",
"prettier": "^2.6.2",
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11885,10 +11885,10 @@ jest-leak-detector@^27.5.1:
jest-get-type "^27.5.1"
pretty-format "^27.5.1"

jest-localstorage-mock@^2.4.21:
version "2.4.22"
resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.22.tgz#9d70be92bfc591c0be289ee2f71de1b4b2a5ca9b"
integrity sha512-60PWSDFQOS5v7JzSmYLM3dPLg0JLl+2Vc4lIEz/rj2yrXJzegsFLn7anwc5IL0WzJbBa/Las064CHbFg491/DQ==
jest-localstorage-mock@^2.4.26:
version "2.4.26"
resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.26.tgz#7d57fb3555f2ed5b7ed16fd8423fd81f95e9e8db"
integrity sha512-owAJrYnjulVlMIXOYQIPRCCn3MmqI3GzgfZCXdD3/pmwrIvFMXcKVWZ+aMc44IzaASapg0Z4SEFxR+v5qxDA2w==

jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1:
version "27.5.1"
Expand Down

0 comments on commit 6fb68bd

Please sign in to comment.