Skip to content

Commit

Permalink
test(transport-test): add webusb tester build
Browse files Browse the repository at this point in the history
  • Loading branch information
mroz22 committed Aug 14, 2024
1 parent 7bd8d08 commit c39fd9b
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .depcheckrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"node-libs-browser",
"rimraf",
"tslib",
"protobufjs-cli"
"protobufjs-cli",
"buffer"
],
"skip-missing": true
}
57 changes: 56 additions & 1 deletion .github/workflows/test-transport.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: "[Test] transport e2e"

permissions:
id-token: write # for fetching the OIDC token
contents: read # for actions/checkout

on:
schedule:
# Runs at midnight UTC every day at 01:00 AM CET
Expand All @@ -20,7 +24,7 @@ on:
workflow_dispatch:

jobs:
transport-e2e:
transport-e2e-test:
if: github.repository == 'trezor/trezor-suite'
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -49,3 +53,54 @@ jobs:

- name: Run E2E tests (new-bridge:emu)
run: yarn workspace @trezor/transport-test test:e2e:new-bridge:emu

extract-branch:
if: github.repository == 'trezor/trezor-suite'
runs-on: ubuntu-latest
outputs:
branch: ${{ steps.extract_branch.outputs.branch }}
steps:
- name: Extract branch name
id: extract_branch
run: |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
build-deploy:
needs: [extract-branch]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.extract-branch.outputs.branch }}

- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"

- name: Install dependencies
shell: bash
run: |
echo -e "\nenableScripts: false" >> .yarnrc.yml
yarn workspaces focus @trezor/transport -A
- name: Build transport tester
run: |
yarn workspace @trezor/transport-test build:e2e:api:browser
- name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::538326561891:role/gh_actions_trezor_suite_dev_deploy
aws-region: eu-central-1

- name: Upload transport tester
shell: bash
env:
DEPLOY_PATH: s3://dev.suite.sldev.cz/transport-test/${{ needs.extract-branch.outputs.branch }}
run: |
echo "DEPLOY_PATH is set to ${DEPLOY_PATH}"
mkdir -p tmp_build_directory
cp -R ./packages/transport-test/e2e/dist/* tmp_build_directory/
aws s3 sync --delete tmp_build_directory/ "${DEPLOY_PATH}"
1 change: 1 addition & 0 deletions packages/connect/src/device/Device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export class Device extends TypedEmitter<DeviceEvents> {

return this._runInner(() => Promise.resolve({}), options);
}

if (TRANSPORT_ERROR.ABORTED_BY_TIMEOUT === error.message) {
this.unreadableError = 'Connection timeout';
}
Expand Down
1 change: 1 addition & 0 deletions packages/transport-test/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
rules: {
'no-nested-ternary': 'off', // useful in tests..
'no-console': 'off',
},
};
111 changes: 111 additions & 0 deletions packages/transport-test/e2e/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { UsbApi } from '@trezor/transport/src/api/usb';

import { buildMessage, assertMessage, assertSuccess, assertEquals } from './utils';
import { sharedTest, success, info, debug, error } from './shared';

/**
* create api for both node and browser and return it
*/
const setupApisUnderTest = async () => {
let usbInterface: ConstructorParameters<typeof UsbApi>[0]['usbInterface'];

if (typeof window !== 'undefined') {
window.Buffer = (await import('buffer')).Buffer;
usbInterface = window?.navigator?.usb;
} else {
usbInterface = await import('usb').then(lib => {
return new lib.WebUSB({ allowAllDevices: true });
});
}

type ApiTestFixture = {
initArgs: ConstructorParameters<typeof UsbApi>[0];
description: string;
};
const apiTestFixture: ApiTestFixture[] = [];
if (typeof window !== 'undefined') {
apiTestFixture.push({ initArgs: { usbInterface }, description: 'browser' });
} else {
apiTestFixture.push(
{
initArgs: { usbInterface, forceReadSerialOnConnect: true },
description: 'node, forceReadSerialOnConnect: true',
},
{
initArgs: { usbInterface, forceReadSerialOnConnect: false },
description: 'node, forceReadSerialOnConnect: false',
},
);
}

return apiTestFixture.map(f => {
return { api: new UsbApi(f.initArgs), description: f.description };
});
};

const runTests = async () => {
const fixtures = await setupApisUnderTest();

for (const f of fixtures) {
info(`Running tests for ${f.description}`);

const { api } = f;
let path: string;

const getConnectedDevicePath = async () => {
info('getConnectedDevicePath...');
const res = await api.enumerate();
debug('getConnectedDevicePath: discovered devices', res);

assertSuccess(res);
if (res.payload.length !== 1) {
throw new Error(error('Expected exactly one device to be connected'));
}
debug('getConnectedDevicePath: path set to: ', res.payload[0].path);
path = res.payload[0].path;
};

const pingPong = async () => {
await api.openDevice(path, true);
const writeResponse = await api.write(path, buildMessage('PING'));
debug('writeResponse', writeResponse);
assertSuccess(writeResponse);

const readResponse = await api.read(path);

debug('readResponse', readResponse);
assertSuccess(readResponse);
debug('readResponse', readResponse.payload.toString('hex'));
assertMessage(readResponse.payload, 'SUCCESS');
await api.closeDevice(path);
};

const concurrentEnumerate = async (concurrent: number) => {
const res = await Promise.all(
new Array(concurrent).fill(undefined).map(() => api.enumerate()),
);
res.forEach((r, index) => {
assertSuccess(r);
assertSuccess(res[0]);
if (index > 0) {
assertEquals(r.payload, res[0].payload);
}
});
};

// interestingly, in node, concurrent enumeration does not work if triggered as the first interaction with connected device.
// concurrent enumeration triggered couple of lines below works correctly
await sharedTest('concurrent enumerate - as the first operation', () =>
// todo: 3 doesn't work in node!!! FIX
concurrentEnumerate(typeof window === 'undefined' ? 3 : 1),
);
await getConnectedDevicePath();

// here concurrent enumeration works correctly, even in node.
await sharedTest('concurrent enumerate', () => concurrentEnumerate(3));
await sharedTest('ping pong', pingPong);
}
success('All tests passed');
};

runTests();
33 changes: 33 additions & 0 deletions packages/transport-test/e2e/api/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const log = (level: 'debug' | 'info' | 'success' | 'error', ...args: any) => {
if (typeof window !== 'undefined') {
// append logs to div class "logs" element
const logs = document.querySelector('.logs');
if (logs) {
logs.innerHTML += `<p style="${level === 'debug' ? 'margin-left:20px;font-size:10px' : ''}">${level === 'success' ? '✅ ' : level === 'error' ? '❌ ' : ''}${args.map((a: any) => (typeof a === 'object' ? JSON.stringify(a) : a))}</p>`;
}
}
console.log(args);

return args.join(' ');
};

export const debug = (...args: any) => log('debug', ...args);
export const info = (...args: any) => log('info', ...args);
export const error = (...args: any) => log('error', ...args);
export const success = (...args: any) => log('success', ...args);

export const sharedTest = async (description: string, callback: () => any) => {
const timeout = 5000;
try {
info(`🔍 ${description}`);
await Promise.race([
callback(),
new Promise((_, reject) =>
setTimeout(() => reject(`Timeout after ${timeout}`), timeout),
),
]);
success(description);
} catch (e) {
throw new Error(error(e));
}
};
33 changes: 33 additions & 0 deletions packages/transport-test/e2e/api/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { error } from './shared';

export const MAGIC = '3f232300';

export const MESSAGES = {
PING: '01',
SUCCESS: '02',
INITIALIZE: '00',
FEATURES: '11',
} as const;

export const buildMessage = (message: keyof typeof MESSAGES) => {
return Buffer.from(MAGIC + MESSAGES[message], 'hex');
};

export const assertMessage = (message: Buffer, expected: keyof typeof MESSAGES) => {
const assertedChunk = message.toString('hex').substring(0, MAGIC.length + 2);
if (assertedChunk !== `${MAGIC}${MESSAGES[expected]}`) {
throw new Error(error(`Expected message ${expected} but got ${assertedChunk}`));
}
};
export function assertSuccess(result: any): asserts result is { success: true; payload: any } {
if (!result.success) {
throw new Error(error(result.error));
}
}
export const assertEquals = (a: any, b: any) => {
const strA = JSON.stringify(a);
const strB = JSON.stringify(b);
if (strA !== strB) {
throw new Error(error(`Expected ${strA} to be equal to ${strB}`));
}
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as messages from '@trezor/protobuf/messages.json';
import { BridgeTransport } from '@trezor/transport';

import { controller as TrezorUserEnvLink } from '../controller';
import { pathLength, descriptor as expectedDescriptor } from '../expect';
import { controller as TrezorUserEnvLink } from './controller';
import { pathLength, descriptor as expectedDescriptor } from './expect';

// todo: introduce global jest config for e2e
jest.setTimeout(60000);
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const config: Config = {
moduleFileExtensions: ['ts', 'js'],
modulePathIgnorePatterns: ['node_modules'],
watchPathIgnorePatterns: ['<rootDir>/libDev', '<rootDir>/lib'],
testPathIgnorePatterns: ['<rootDir>/libDev/', '<rootDir>/lib/', '<rootDir>/tests/'],
testPathIgnorePatterns: ['<rootDir>/libDev/', '<rootDir>/lib/', '<rootDir>/e2e/api'],
transform: {
'\\.(js|ts)$': [
'babel-jest',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as messages from '@trezor/protobuf/messages.json';
import { BridgeTransport, Descriptor } from '@trezor/transport';

import { controller as TrezorUserEnvLink } from '../controller';
import { descriptor as fixtureDescriptor } from '../expect';
import { controller as TrezorUserEnvLink } from './controller';
import { descriptor as fixtureDescriptor } from './expect';

// todo: introduce global jest config for e2e
jest.setTimeout(60000);
Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions packages/transport-test/e2e/ui/api.test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<body style="background-color: black; color: white; font-family: monospace">
<script type="module" src="./api.test.browser.js"></script>
<h1>Webusb tester</h1>
<div>connect device, pair device and reload page.</div>
<button onclick="navigator.usb.requestDevice({filters: []})">pair</button>
<hr />
<div class="logs"></div>
</body>
</html>
16 changes: 11 additions & 5 deletions packages/transport-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@
"depcheck": "yarn g:depcheck",
"lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'",
"type-check": "yarn g:tsc --build",
"test:e2e": "ts-node -O '{\"module\": \"commonjs\", \"esModuleInterop\": true}' ./e2e/run.ts",
"test:e2e:old-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=false yarn test:e2e",
"test:e2e:old-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=false yarn test:e2e",
"test:e2e:new-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=true yarn test:e2e",
"test:e2e:new-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=true yarn test:e2e"
"test:e2e:bridge": "ts-node -O '{\"module\": \"commonjs\", \"esModuleInterop\": true}' ./e2e/bridge/run.ts",
"test:e2e:old-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=false yarn test:e2e:bridge",
"test:e2e:old-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=false yarn test:e2e:bridge",
"test:e2e:new-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=true yarn test:e2e:bridge",
"test:e2e:new-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=true yarn test:e2e:bridge",
"build:e2e:api:node": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.node.js --platform=node --target=node18 --external:usb",
"build:e2e:api:browser": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.browser.js --platform=browser --external:usb && cp e2e/ui/api.test.html e2e/dist/index.html",
"test:e2e:api:node:hw": "yarn build:e2e:api:node && node ./e2e/dist/api.test.node.js",
"test:e2e:api:browser:hw": "yarn build:e2e:api:browser && npx http-serve ./e2e/dist"
},
"devDependencies": {
"@jest/types": "^29.6.3",
"@trezor/transport": "workspace:*",
"@trezor/transport-bridge": "workspace:*",
"@trezor/trezor-user-env-link": "workspace:^",
"@trezor/utils": "workspace:*",
"buffer": "^6.0.3",
"esbuild": "^0.20.0",
"jest": "^29.7.0",
"ts-node": "^10.9.1",
"usb": "^2.11.0"
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11654,6 +11654,8 @@ __metadata:
"@trezor/transport-bridge": "workspace:*"
"@trezor/trezor-user-env-link": "workspace:^"
"@trezor/utils": "workspace:*"
buffer: "npm:^6.0.3"
esbuild: "npm:^0.20.0"
jest: "npm:^29.7.0"
ts-node: "npm:^10.9.1"
usb: "npm:^2.11.0"
Expand Down

0 comments on commit c39fd9b

Please sign in to comment.