diff --git a/apps/sample/src/components/ApduView/index.tsx b/apps/sample/src/components/ApduView/index.tsx index 11f19a4fc..92aef352f 100644 --- a/apps/sample/src/components/ApduView/index.tsx +++ b/apps/sample/src/components/ApduView/index.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState } from "react"; +import { ApduResponse } from "@ledgerhq/device-sdk-core"; import { Button, Divider, Flex, Grid, Input, Text } from "@ledgerhq/react-ui"; import styled, { DefaultTheme } from "styled-components"; @@ -56,9 +57,16 @@ const InputContainer = styled(Flex).attrs({ mx: 8, mb: 4 })` const inputContainerProps = { style: { borderRadius: 4 } }; +const ResultDescText = styled(Text).attrs({ variant: "body", mt: 4, px: 8 })` + min-width: 150px; + display: inline-block; +`; + export const ApduView: React.FC = () => { - const { apduFormValues, setApduFormValue, getRawApdu } = useApduForm(); + const { apduFormValues, setApduFormValue, getRawApdu, getHexString } = + useApduForm(); const [loading, setLoading] = useState(false); + const [apduResponse, setApduResponse] = useState(); const sdk = useSdk(); const { state: { selectedId: selectedSessionId }, @@ -66,11 +74,14 @@ export const ApduView: React.FC = () => { const onSubmit = useCallback( async (values: typeof apduFormValues) => { setLoading(true); + let rawApduResponse; try { - await sdk.sendApdu({ + rawApduResponse = await sdk.sendApdu({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion sessionId: selectedSessionId!, apdu: getRawApdu(values), }); + setApduResponse(rawApduResponse); setLoading(false); } catch (error) { setLoading(false); @@ -161,7 +172,24 @@ export const ApduView: React.FC = () => { - + + {apduResponse && ( + <> + + Raw APDU: + {getHexString(getRawApdu(apduFormValues))} + + + Response status: + {getHexString(apduResponse.statusCode)} + + + Response raw data: + {getHexString(apduResponse.data)} + + + + )} onSubmit(apduFormValues)} diff --git a/apps/sample/src/components/Device/index.tsx b/apps/sample/src/components/Device/index.tsx index a0fa4ab1b..6ec2558fd 100644 --- a/apps/sample/src/components/Device/index.tsx +++ b/apps/sample/src/components/Device/index.tsx @@ -43,7 +43,11 @@ export const Device: React.FC = ({ return ( - {model === "stax" ? : } + {model === DeviceModelId.STAX ? ( + + ) : ( + + )} {name} diff --git a/apps/sample/src/hooks/useApduForm.ts b/apps/sample/src/hooks/useApduForm.ts index 116cc1fd1..999c9506a 100644 --- a/apps/sample/src/hooks/useApduForm.ts +++ b/apps/sample/src/hooks/useApduForm.ts @@ -43,10 +43,17 @@ export function useApduForm() { [], ); + const getHexString = useCallback((raw: Uint8Array): string => { + return raw + .reduce((acc, curr) => acc + " " + curr.toString(16).padStart(2, "0"), "") + .toUpperCase(); + }, []); + return { apduFormValues: values, setApduFormValue: setValue, getRawApdu, + getHexString, }; } diff --git a/apps/sample/src/providers/SessionsProvider/index.tsx b/apps/sample/src/providers/SessionsProvider/index.tsx index 7caeca890..e73f6db7e 100644 --- a/apps/sample/src/providers/SessionsProvider/index.tsx +++ b/apps/sample/src/providers/SessionsProvider/index.tsx @@ -15,7 +15,7 @@ type SessionContextType = { const SessionContext: Context = createContext({ state: SessionsInitialState, - dispatch: (_value: AddSessionAction) => {}, + dispatch: (_value: AddSessionAction) => null, }); export const SessionProvider: React.FC = ({ diff --git a/packages/core/src/api/DeviceSdk.test.ts b/packages/core/src/api/DeviceSdk.test.ts index aed02851c..b701aee0e 100644 --- a/packages/core/src/api/DeviceSdk.test.ts +++ b/packages/core/src/api/DeviceSdk.test.ts @@ -1,7 +1,11 @@ import { LocalConfigDataSource } from "@internal/config/data/ConfigDataSource"; import { StubLocalConfigDataSource } from "@internal/config/data/LocalConfigDataSource.stub"; import { configTypes } from "@internal/config/di/configTypes"; +import { discoveryTypes } from "@internal/discovery/di/discoveryTypes"; +import { sendTypes } from "@internal/send/di/sendTypes"; +import { usbDiTypes } from "@internal/usb/di/usbDiTypes"; import pkg from "@root/package.json"; +import { StubUseCase } from "@root/src/di.stub"; import { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger"; import { DeviceSdk } from "./DeviceSdk"; @@ -26,12 +30,24 @@ describe("DeviceSdk", () => { expect(await sdk.getVersion()).toBe(pkg.version); }); - it("startScan should ....", () => { - expect(sdk.startScan()).toBeFalsy(); + it("should have startDiscovery method", () => { + expect(sdk.startDiscovering).toBeDefined(); }); - it("stopScan should ....", () => { - expect(sdk.stopScan()).toBeFalsy(); + it("should have stopDiscovery method", () => { + expect(sdk.stopDiscovering).toBeDefined(); + }); + + it("should have connect method", () => { + expect(sdk.connect).toBeDefined(); + }); + + it("should have sendApdu method", () => { + expect(sdk.sendApdu).toBeDefined(); + }); + + it("should have getConnectedDevice method", () => { + expect(sdk.getConnectedDevice).toBeDefined(); }); }); @@ -40,9 +56,12 @@ describe("DeviceSdk", () => { sdk = new DeviceSdk({ stub: true, loggers: [] }); }); - it("should create a stubbed version", () => { + it("should create a stubbed sdk", () => { expect(sdk).toBeDefined(); expect(sdk).toBeInstanceOf(DeviceSdk); + }); + + it("should return a stubbed config", () => { expect( sdk.container.get( configTypes.LocalConfigDataSource, @@ -50,9 +69,21 @@ describe("DeviceSdk", () => { ).toBeInstanceOf(StubLocalConfigDataSource); }); - it("should return a stubbed `version`", async () => { + it("should return a stubbed version", async () => { expect(await sdk.getVersion()).toBe("0.0.0-stub.1"); }); + + it.each([ + [discoveryTypes.StartDiscoveringUseCase], + [discoveryTypes.StopDiscoveringUseCase], + [discoveryTypes.ConnectUseCase], + [sendTypes.SendApduUseCase], + [usbDiTypes.GetConnectedDeviceUseCase], + ])("should have %p use case", (diSymbol) => { + const uc = sdk.container.get(diSymbol); + expect(uc).toBeInstanceOf(StubUseCase); + expect(uc.execute()).toBe("stub"); + }); }); describe("without args", () => { diff --git a/packages/core/src/api/DeviceSdk.ts b/packages/core/src/api/DeviceSdk.ts index a7ace6ba3..485d3dae1 100644 --- a/packages/core/src/api/DeviceSdk.ts +++ b/packages/core/src/api/DeviceSdk.ts @@ -36,14 +36,6 @@ export class DeviceSdk { this.container = makeContainer({ stub, loggers }); } - startScan() { - return; - } - - stopScan() { - return; - } - getVersion(): Promise { return this.container .get(configTypes.GetSdkVersionUseCase) diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 63ac9f71b..49f78b6f8 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -6,3 +6,4 @@ export { LogLevel } from "./logger-subscriber/model/LogLevel"; export { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger"; export * from "./types"; export { ConnectedDevice } from "./usb/model/ConnectedDevice"; +export { ApduResponse } from "@internal/device-session/model/ApduResponse"; diff --git a/packages/core/src/api/types.ts b/packages/core/src/api/types.ts index f6767eee7..384c0a79d 100644 --- a/packages/core/src/api/types.ts +++ b/packages/core/src/api/types.ts @@ -2,8 +2,8 @@ export type { LogSubscriberOptions } from "./logger-subscriber/model/LogSubscrib export type { DeviceId, DeviceModel, - DeviceModelId, } from "@internal/device-model/model/DeviceModel"; +export { DeviceModelId } from "@internal/device-model/model/DeviceModel"; export type { SessionId } from "@internal/device-session/model/Session"; export type { ConnectionType } from "@internal/discovery/model/ConnectionType"; export type { DiscoveredDevice } from "@internal/usb/model/DiscoveredDevice"; diff --git a/packages/core/src/di.stub.ts b/packages/core/src/di.stub.ts new file mode 100644 index 000000000..b4c04f5bf --- /dev/null +++ b/packages/core/src/di.stub.ts @@ -0,0 +1,6 @@ +import { injectable } from "inversify"; + +@injectable() +export class StubUseCase { + execute = jest.fn(() => "stub"); +} diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index bab0d0fe9..90fb8e264 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -35,7 +35,7 @@ export const makeContainer = ({ discoveryModuleFactory({ stub }), loggerModuleFactory({ subscribers: loggers }), deviceSessionModuleFactory(), - sendModuleFactory(), + sendModuleFactory({ stub }), // modules go here ); diff --git a/packages/core/src/internal/device-session/model/ApduResponse.ts b/packages/core/src/internal/device-session/model/ApduResponse.ts index 6f05fcaec..87c96f61f 100644 --- a/packages/core/src/internal/device-session/model/ApduResponse.ts +++ b/packages/core/src/internal/device-session/model/ApduResponse.ts @@ -4,11 +4,11 @@ export type ApduResponseConstructorArgs = { }; export class ApduResponse { - protected _statusCode: Uint8Array; - protected _data: Uint8Array; + public statusCode: Uint8Array; + public data: Uint8Array; constructor({ statusCode, data }: ApduResponseConstructorArgs) { - this._statusCode = statusCode; - this._data = data; + this.statusCode = statusCode; + this.data = data; } } diff --git a/packages/core/src/internal/device-session/model/Session.ts b/packages/core/src/internal/device-session/model/Session.ts index a73233195..53868f6dc 100644 --- a/packages/core/src/internal/device-session/model/Session.ts +++ b/packages/core/src/internal/device-session/model/Session.ts @@ -2,7 +2,7 @@ import { v4 as uuidv4 } from "uuid"; import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; -export type SessionId = ReturnType; +export type SessionId = string; export type SessionConstructorArgs = { connectedDevice: InternalConnectedDevice; diff --git a/packages/core/src/internal/discovery/di/discoveryModule.ts b/packages/core/src/internal/discovery/di/discoveryModule.ts index d26c3e00b..096e0cdf8 100644 --- a/packages/core/src/internal/discovery/di/discoveryModule.ts +++ b/packages/core/src/internal/discovery/di/discoveryModule.ts @@ -3,6 +3,7 @@ import { ContainerModule } from "inversify"; import { ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase"; import { StartDiscoveringUseCase } from "@internal/discovery/use-case/StartDiscoveringUseCase"; import { StopDiscoveringUseCase } from "@internal/discovery/use-case/StopDiscoveringUseCase"; +import { StubUseCase } from "@root/src/di.stub"; import { discoveryTypes } from "./discoveryTypes"; @@ -13,13 +14,14 @@ type FactoryProps = { export const discoveryModuleFactory = ({ stub = false, }: Partial = {}) => - new ContainerModule((bind, _unbind, _isBound, _rebind) => { + new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(discoveryTypes.StartDiscoveringUseCase).to(StartDiscoveringUseCase); bind(discoveryTypes.StopDiscoveringUseCase).to(StopDiscoveringUseCase); bind(discoveryTypes.ConnectUseCase).to(ConnectUseCase); if (stub) { - // We can rebind our interfaces to their mock implementations - // rebind(...).to(....); + rebind(discoveryTypes.StartDiscoveringUseCase).to(StubUseCase); + rebind(discoveryTypes.StopDiscoveringUseCase).to(StubUseCase); + rebind(discoveryTypes.ConnectUseCase).to(StubUseCase); } }); diff --git a/packages/core/src/internal/send/di/sendModule.ts b/packages/core/src/internal/send/di/sendModule.ts index 6fb233ebc..8bc2c7e1c 100644 --- a/packages/core/src/internal/send/di/sendModule.ts +++ b/packages/core/src/internal/send/di/sendModule.ts @@ -1,6 +1,7 @@ import { ContainerModule } from "inversify"; import { SendApduUseCase } from "@internal/send/use-case/SendApduUseCase"; +import { StubUseCase } from "@root/src/di.stub"; import { sendTypes } from "./sendTypes"; @@ -8,17 +9,22 @@ type FactoryProps = { stub: boolean; }; -export const sendModuleFactory = (_args: Partial = {}) => +export const sendModuleFactory = ({ + stub = false, +}: Partial = {}) => new ContainerModule( ( bind, _unbind, _isBound, - _rebind, + rebind, _unbindAsync, _onActivation, _onDeactivation, ) => { bind(sendTypes.SendApduUseCase).to(SendApduUseCase); + if (stub) { + rebind(sendTypes.SendApduUseCase).to(StubUseCase); + } }, ); diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts index 1c14eafab..280db169f 100644 --- a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts +++ b/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts @@ -1,4 +1,9 @@ -import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; +import { Left } from "purify-ts"; + +import { + DeviceSessionNotFound, + ReceiverApduError, +} from "@internal/device-session/model/Errors"; import { sessionStubBuilder } from "@internal/device-session/model/Session.stub"; import { DefaultSessionService } from "@internal/device-session/service/DefaultSessionService"; import { SessionService } from "@internal/device-session/service/SessionService"; @@ -48,12 +53,12 @@ describe("SendApduUseCase", () => { await expect(response).rejects.toBeInstanceOf(DeviceSessionNotFound); }); - it("should throw an error if the apdu receiver failed", () => { + it("should throw an error if the apdu receiver failed", async () => { // given const connectedDevice = connectedDeviceStubBuilder({ - sendApdu: jest.fn(async () => { - throw new Error("apdu receiver failed"); - }), + sendApdu: jest.fn(async () => + Promise.resolve(Left(new ReceiverApduError())), + ), }); const session = sessionStubBuilder({ connectedDevice }); sessionService.addSession(session); @@ -66,6 +71,6 @@ describe("SendApduUseCase", () => { }); // then - expect(response).rejects.toThrow("apdu receiver failed"); + await expect(response).rejects.toBeInstanceOf(ReceiverApduError); }); }); diff --git a/packages/core/src/internal/usb/di/usbModule.ts b/packages/core/src/internal/usb/di/usbModule.ts index 602fb4e6b..407344293 100644 --- a/packages/core/src/internal/usb/di/usbModule.ts +++ b/packages/core/src/internal/usb/di/usbModule.ts @@ -3,6 +3,7 @@ import { ContainerModule } from "inversify"; import { UsbHidDeviceConnectionFactory } from "@internal/usb/service/UsbHidDeviceConnectionFactory"; import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport"; import { GetConnectedDeviceUseCase } from "@internal/usb/use-case/GetConnectedDeviceUseCase"; +import { StubUseCase } from "@root/src/di.stub"; import { usbDiTypes } from "./usbDiTypes"; @@ -13,7 +14,7 @@ type FactoryProps = { export const usbModuleFactory = ({ stub = false, }: Partial = {}) => - new ContainerModule((bind, _unbind, _isBound, _rebind) => { + new ContainerModule((bind, _unbind, _isBound, rebind) => { // The transport needs to be a singleton to keep the internal states of the devices bind(usbDiTypes.UsbHidTransport).to(WebUsbHidTransport).inSingletonScope(); @@ -26,7 +27,6 @@ export const usbModuleFactory = ({ bind(usbDiTypes.GetConnectedDeviceUseCase).to(GetConnectedDeviceUseCase); if (stub) { - // We can rebind our interfaces to their mock implementations - // rebind(...).to(....); + rebind(usbDiTypes.GetConnectedDeviceUseCase).to(StubUseCase); } }); diff --git a/packages/core/src/internal/usb/model/InternalConnectedDevice.test.ts b/packages/core/src/internal/usb/model/InternalConnectedDevice.test.ts new file mode 100644 index 000000000..c6afaf37d --- /dev/null +++ b/packages/core/src/internal/usb/model/InternalConnectedDevice.test.ts @@ -0,0 +1,37 @@ +import { deviceModelStubBuilder } from "@internal/device-model/model/DeviceModel.stub"; +import { defaultApduResponseStubBuilder } from "@internal/device-session/model/ApduResponse.stub"; +import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; +import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub"; + +describe("InternalConnectedDevice", () => { + let connectedDevice: InternalConnectedDevice; + + beforeEach(() => { + connectedDevice = connectedDeviceStubBuilder(); + }); + + it("should create an instance", () => { + expect(connectedDevice).toBeDefined(); + expect(connectedDevice).toBeInstanceOf(InternalConnectedDevice); + }); + + it("should return the correct id", () => { + expect(connectedDevice.id).toEqual("42"); + }); + + it("should return the correct device model", () => { + expect(JSON.stringify(connectedDevice.deviceModel)).toEqual( + JSON.stringify(deviceModelStubBuilder()), + ); + }); + + it("should return the correct type", () => { + expect(connectedDevice.type).toEqual("MOCK"); + }); + + it("should return the correct send apdu response", () => { + expect(connectedDevice.sendApdu(new Uint8Array())).toMatchObject( + Promise.resolve(defaultApduResponseStubBuilder()), + ); + }); +}); diff --git a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts b/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts index a39482d70..62eb03f77 100644 --- a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts +++ b/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts @@ -20,7 +20,7 @@ describe("UsbHidDeviceConnection", () => { let apduReceiver: ApduReceiverService; const logger = (tag: string) => new DefaultLoggerPublisherService([], tag); - beforeEach(async () => { + beforeEach(() => { device = hidDeviceStubBuilder(); apduSender = defaultApduSenderServiceStubBuilder(undefined, logger); apduReceiver = defaultApduReceiverServiceStubBuilder(undefined, logger); @@ -45,19 +45,21 @@ describe("UsbHidDeviceConnection", () => { logger, ); // when - connection.sendApdu(new Uint8Array(0)); + void connection.sendApdu(new Uint8Array(0)); // then expect(device.sendReport).toHaveBeenCalled(); }); it("should receive APDU through hid report", async () => { // given - device.sendReport = jest.fn(async () => { - device.oninputreport!({ - type: "inputreport", - data: new DataView(Uint8Array.from(RESPONSE_LOCKED_DEVICE).buffer), - } as HIDInputReportEvent); - }); + device.sendReport = jest.fn(() => + Promise.resolve( + device.oninputreport!({ + type: "inputreport", + data: new DataView(Uint8Array.from(RESPONSE_LOCKED_DEVICE).buffer), + } as HIDInputReportEvent), + ), + ); const connection = new UsbHidDeviceConnection( { device, apduSender, apduReceiver }, logger, @@ -65,6 +67,6 @@ describe("UsbHidDeviceConnection", () => { // when const response = connection.sendApdu(Uint8Array.from([])); // then - expect(response).resolves.toBe(RESPONSE_LOCKED_DEVICE); + void expect(response).resolves.toBe(RESPONSE_LOCKED_DEVICE); }); });