Skip to content

Commit

Permalink
Wallet Sync - Watch Loop Unit Tests (#7517)
Browse files Browse the repository at this point in the history
support(live-wallet): Added unit tests to cover the createWalletSyncWatchLoop
  • Loading branch information
cgrellard-ledger authored Aug 6, 2024
1 parent 8fe8b24 commit 277648c
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-eggs-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-wallet": patch
---

LIVE-WALLET - Added unit tests to cover the createWalletSyncWatchLoop
1 change: 1 addition & 0 deletions libs/live-wallet/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
"^.+\\.(ts|tsx)?$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
globals: {
isolatedModules: true,
},
Expand Down
14 changes: 13 additions & 1 deletion libs/live-wallet/src/cloudsync/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@ export type UpdateEvent<Data> =
type: "deleted-data";
};

export class CloudSyncSDK<Schema extends ZodType, Data = z.infer<Schema>> {
export interface CloudSyncSDKInterface<Data> {
push(trustchain: Trustchain, memberCredentials: MemberCredentials, data: Data): Promise<void>;
pull(trustchain: Trustchain, memberCredentials: MemberCredentials): Promise<void>;
destroy(trustchain: Trustchain, memberCredentials: MemberCredentials): Promise<void>;
listenNotifications(
trustchain: Trustchain,
memberCredentials: MemberCredentials,
): Observable<number>;
}

export class CloudSyncSDK<Schema extends ZodType, Data = z.infer<Schema>>
implements CloudSyncSDKInterface<Data>
{
private slug: string;
private schema: Schema;
private trustchainSdk: TrustchainSDK;
Expand Down
91 changes: 91 additions & 0 deletions libs/live-wallet/src/walletsync/__mocks__/watchLoop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { CloudSyncSDKInterface } from "../../cloudsync";
import { z } from "zod";
import { Observable, never } from "rxjs";
import { WalletSyncDataManager } from "../types";
import { Trustchain, MemberCredentials } from "@ledgerhq/trustchain/types";
import { CreateWalletSyncWatchLoopParams } from "../createWalletSyncWatchLoop";

export const getWalletSyncWatchLoopConfig = () => {
const schema = z.number();
type Data = number;

const walletsync: WalletSyncDataManager<number, number, typeof schema> = {
schema,
diffLocalToDistant(local, latest) {
return { hasChanges: local !== (latest || 0), nextState: local };
},
async resolveIncrementalUpdate(_ctx, local, latest, incoming) {
if (incoming === latest || !incoming) {
return { hasChanges: false };
}
return {
hasChanges: true,
update: incoming,
};
},
applyUpdate(_local, update) {
return update;
},
};

const trustchain = {
rootId: "",
applicationPath: "",
walletSyncEncryptionKey: "",
};

const memberCredentials = {
pubkey: "",
privatekey: "",
};

const localIncrementUpdate = jest.fn();

const walletSyncSdk: CloudSyncSDKInterface<Data> = {
pull: jest.fn().mockResolvedValue(undefined),
push: jest.fn().mockResolvedValue(undefined),
destroy: jest.fn().mockResolvedValue(undefined),
listenNotifications(
_trustchain: Trustchain,
_memberCredentials: MemberCredentials,
): Observable<number> {
return never();
},
};

const getState = jest.fn().mockReturnValue(null);
const localStateSelector = jest.fn().mockReturnValue(0);
const latestDistantStateSelector = jest.fn().mockReturnValue(null);
const onTrustchainRefreshNeeded = jest.fn();

const walletSyncWatchLoopConfig: CreateWalletSyncWatchLoopParams<
number,
number,
number,
typeof schema
> = {
walletsync,
walletSyncSdk,
trustchain,
memberCredentials,
getState,
localStateSelector,
latestDistantStateSelector,
onTrustchainRefreshNeeded,
localIncrementUpdate,
};

return walletSyncWatchLoopConfig;
};

/**
* workaround on Jest advanceTimersByTime to awaits promises that arises inside setTimeouts
* see https://stackoverflow.com/questions/51126786/jest-fake-timers-with-promises
*/
export async function advanceTimersByTimeAsync(seconds: number) {
const timeIncrement = 1000;
for (let t = 0; t < 1000 * seconds; t += timeIncrement) {
jest.advanceTimersByTime(timeIncrement);
await Promise.resolve();
}
}
104 changes: 104 additions & 0 deletions libs/live-wallet/src/walletsync/createWalletSyncWatchLoop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { createWalletSyncWatchLoop } from "./createWalletSyncWatchLoop";
import { advanceTimersByTimeAsync, getWalletSyncWatchLoopConfig } from "./__mocks__/watchLoop";
import { Subject } from "rxjs";

jest.useFakeTimers();

describe("createWalletSyncWatchLoop", () => {
it("should pull but not push when there is no changes", async () => {
const walletSyncWatchLoopConfig = getWalletSyncWatchLoopConfig();

const watchLoop = createWalletSyncWatchLoop(walletSyncWatchLoopConfig);

await advanceTimersByTimeAsync(10);

watchLoop.unsubscribe();

expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(1);
expect(walletSyncWatchLoopConfig.walletSyncSdk.push).toHaveBeenCalledTimes(0);
});

it("should pull and push when there are changes", async () => {
const walletSyncWatchLoopConfig = getWalletSyncWatchLoopConfig();

walletSyncWatchLoopConfig.latestDistantStateSelector = jest
.fn()
.mockReturnValueOnce(0)
.mockReturnValue(1);
walletSyncWatchLoopConfig.localStateSelector = jest.fn().mockReturnValue(1);

const watchLoop = createWalletSyncWatchLoop(walletSyncWatchLoopConfig);

await advanceTimersByTimeAsync(20);

watchLoop.unsubscribe();

expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(2);
expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledWith(
walletSyncWatchLoopConfig.trustchain,
walletSyncWatchLoopConfig.memberCredentials,
);
expect(walletSyncWatchLoopConfig.walletSyncSdk.push).toHaveBeenCalledTimes(1);
expect(walletSyncWatchLoopConfig.walletSyncSdk.push).toHaveBeenCalledWith(
walletSyncWatchLoopConfig.trustchain,
walletSyncWatchLoopConfig.memberCredentials,
1,
);
});

it("notifications triggers the watch loop", async () => {
const walletSyncWatchLoopConfig = getWalletSyncWatchLoopConfig();

const notifications = new Subject<number>();

walletSyncWatchLoopConfig.walletSyncSdk.listenNotifications = (t, m) => {
expect(t).toEqual(walletSyncWatchLoopConfig.trustchain);
expect(m).toEqual(walletSyncWatchLoopConfig.memberCredentials);
return notifications;
};

walletSyncWatchLoopConfig.watchConfig = {
notificationsEnabled: true,
initialTimeout: 5000,
pollingInterval: 10000,
};

const watchLoop = createWalletSyncWatchLoop(walletSyncWatchLoopConfig);

await advanceTimersByTimeAsync(15);
expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(2);

notifications.next(2);

await advanceTimersByTimeAsync(1);
expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(3);

watchLoop.unsubscribe();
});

it("notifications disabled does not triggers the watch loop", async () => {
const walletSyncWatchLoopConfig = getWalletSyncWatchLoopConfig();

const notifications = new Subject<number>();

walletSyncWatchLoopConfig.walletSyncSdk.listenNotifications = () => notifications;

walletSyncWatchLoopConfig.watchConfig = {
notificationsEnabled: false,
initialTimeout: 5000,
pollingInterval: 10000,
};

const watchLoop = createWalletSyncWatchLoop(walletSyncWatchLoopConfig);

await advanceTimersByTimeAsync(15);
expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(2);

notifications.next(2);

await advanceTimersByTimeAsync(1);
expect(walletSyncWatchLoopConfig.walletSyncSdk.pull).toHaveBeenCalledTimes(2);

watchLoop.unsubscribe();
});
});
50 changes: 26 additions & 24 deletions libs/live-wallet/src/walletsync/createWalletSyncWatchLoop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ZodType, z } from "zod";
import { CloudSyncSDK } from "../cloudsync";
import { CloudSyncSDKInterface } from "../cloudsync";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { WalletSyncDataManager } from "./types";
import { log } from "@ledgerhq/logs";
Expand All @@ -17,32 +17,13 @@ export type VisualConfig = {
visualPendingTimeout: number;
};

/**
* createWalletSyncWatchLoop is a helper to create a watch loop that will automatically sync the wallet with the cloud sync backend.
* make sure to unsubscribe if you need to rerun a new watch loop. notably if one of the input changes.
*/
export function createWalletSyncWatchLoop<
export type CreateWalletSyncWatchLoopParams<
UserState,
LocalState,
Update,
Schema extends ZodType,
DistantState = z.infer<Schema>,
>({
watchConfig,
visualConfig,
walletsync,
walletSyncSdk,
trustchain,
memberCredentials,
setVisualPending,
onStartPolling,
onTrustchainRefreshNeeded,
onError,
getState,
localStateSelector,
latestDistantStateSelector,
localIncrementUpdate,
}: {
> = {
/**
* the configuration to use to watch for changes.
*/
Expand All @@ -58,7 +39,7 @@ export function createWalletSyncWatchLoop<
/**
* the wallet sync sdk to use to interact with the cloud sync backend.
*/
walletSyncSdk: CloudSyncSDK<Schema>;
walletSyncSdk: CloudSyncSDKInterface<DistantState>;
/**
* the trustchain to use to authenticate with the cloud sync backend.
*/
Expand Down Expand Up @@ -110,7 +91,28 @@ export function createWalletSyncWatchLoop<
* a function we need to regularly call to also resolve possible local state updates. (see incrementalUpdates.ts)
*/
localIncrementUpdate: () => Promise<void>;
}): {
};

/**
* createWalletSyncWatchLoop is a helper to create a watch loop that will automatically sync the wallet with the cloud sync backend.
* make sure to unsubscribe if you need to rerun a new watch loop. notably if one of the input changes.
*/
export function createWalletSyncWatchLoop<UserState, LocalState, Update, Schema extends ZodType>({
watchConfig,
visualConfig,
walletsync,
walletSyncSdk,
trustchain,
memberCredentials,
setVisualPending,
onStartPolling,
onTrustchainRefreshNeeded,
onError,
getState,
localStateSelector,
latestDistantStateSelector,
localIncrementUpdate,
}: CreateWalletSyncWatchLoopParams<UserState, LocalState, Update, Schema>): {
onUserRefreshIntent: () => void;
unsubscribe: () => void;
} {
Expand Down

0 comments on commit 277648c

Please sign in to comment.