committed by
GitHub
49 changed files with 686 additions and 180 deletions
@ -1 +1,2 @@ |
|||||
package.json |
package.json |
||||
|
test-e2e/**/*.json |
||||
|
@ -0,0 +1,18 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { of } from 'rxjs' |
||||
|
|
||||
|
type Input = void |
||||
|
type Result = boolean |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand('killInternalProcess', () => { |
||||
|
setTimeout(() => { |
||||
|
// we assume commands are run on the internal process
|
||||
|
// special exit code for better identification
|
||||
|
process.exit(42) |
||||
|
}) |
||||
|
return of(true) |
||||
|
}) |
||||
|
|
||||
|
export default cmd |
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,15 @@ |
|||||
|
# ledgerLive-QA |
||||
|
Automated tests for Ledger Live Desktop application. |
||||
|
Start Ledger Live Desktop application with accounts for the supported coin. Operations history removed from db. Then sync to retrieve account balance and transactions history. |
||||
|
|
||||
|
|
||||
|
## Accounts setup and sync |
||||
|
#### Launch test |
||||
|
yarn test-sync |
||||
|
|
||||
|
#### Test description |
||||
|
Clean Ledger Live Application settings directory. |
||||
|
Copy app.json init file for testing in a new Ledger Live Application settings directory. |
||||
|
Start Ledger Live Desktop app. |
||||
|
Wait for sync OK. |
||||
|
Compare new app.json with expected app.json file. |
@ -0,0 +1,93 @@ |
|||||
|
import { Application } from 'spectron' |
||||
|
import { waitForDisappear, waitForExpectedText } from './helpers' |
||||
|
|
||||
|
const os = require('os') |
||||
|
const path = require('path') |
||||
|
const fs = require('fs') |
||||
|
const appVersion = require('../package.json') |
||||
|
|
||||
|
let app |
||||
|
|
||||
|
const TIMEOUT = 50 * 1000 |
||||
|
|
||||
|
let appPath |
||||
|
let configPath |
||||
|
const platform = os.platform() |
||||
|
if (platform === 'darwin') { |
||||
|
appPath = `./dist/mac/Ledger Live.app/Contents/MacOS/Ledger Live` |
||||
|
configPath = `${os.homedir()}/Library/Application Support/Ledger Live/` |
||||
|
} else if (platform === 'win32') { |
||||
|
appPath = `.\\dist\\win-unpacked\\Ledger Live.exe` |
||||
|
configPath = '%AppData\\Roaming\\Ledger Live' |
||||
|
} else { |
||||
|
appPath = `./dist/ledger-live-desktop-${appVersion.version}-linux-x86_64.AppImage` |
||||
|
configPath = '$HOME/apps/ledger-live-desktop-$ledgerLiveVersion-linux-x86_64.AppImage' |
||||
|
} |
||||
|
|
||||
|
describe('Application launch', () => { |
||||
|
beforeEach(async () => { |
||||
|
app = new Application({ |
||||
|
path: appPath, |
||||
|
env: { |
||||
|
SKIP_ONBOARDING: '1', |
||||
|
}, |
||||
|
}) |
||||
|
await app.start() |
||||
|
}, TIMEOUT) |
||||
|
|
||||
|
afterEach(async () => { |
||||
|
if (app && app.isRunning()) { |
||||
|
await app.stop() |
||||
|
} |
||||
|
}, TIMEOUT) |
||||
|
|
||||
|
test( |
||||
|
'Start app, activate password lock, check app.json, deactivate password lock', |
||||
|
async () => { |
||||
|
const title = await app.client.getTitle() |
||||
|
expect(title).toEqual('Ledger Live') |
||||
|
await app.client.waitUntilWindowLoaded() |
||||
|
await waitForDisappear(app, '#preload') |
||||
|
// Verify Account summary text
|
||||
|
// Count user's accounts
|
||||
|
const userAccountsList = await app.client.elements('[data-e2e=dashboard_AccountCardWrapper]') |
||||
|
const userAccountsCount = Object.keys(userAccountsList.value).length |
||||
|
// Check account number
|
||||
|
const accountSummary = await app.client.getText('[data-e2e=dashboard_accountsSummaryDesc]') |
||||
|
const accountSummaryMessage = `Here's the summary of your ${userAccountsCount} accounts` |
||||
|
expect(accountSummary).toEqual(accountSummaryMessage) |
||||
|
|
||||
|
// Go to settings
|
||||
|
await app.client.click('[data-e2e=setting_button]') |
||||
|
await waitForExpectedText(app, '[data-e2e=settings_title]', 'Settings') |
||||
|
|
||||
|
// Enable lock password
|
||||
|
await app.client.click('[data-e2e=passwordLock_button]') |
||||
|
await waitForExpectedText(app, '[data-e2e=setPassword_modalTitle]', 'Set a password') |
||||
|
await app.client.setValue('[data-e2e=setPassword_NewPassword]', 5) |
||||
|
await app.client.setValue('[data-e2e=setPassword_ConfirmPassword]', 5) |
||||
|
await app.client.keys('Enter') |
||||
|
await waitForExpectedText(app, '[data-e2e=settings_title]', 'Settings') |
||||
|
await app.client.pause(2000) |
||||
|
// Verify in app.json that accounts data are encrypted
|
||||
|
const tmpAppJSONPath = path.resolve(configPath, 'app.json') |
||||
|
const LockedfileContent = fs.readFileSync(tmpAppJSONPath, 'utf-8') |
||||
|
const accountsOperations = '"operations":[{' |
||||
|
await expect(LockedfileContent).not.toContain(accountsOperations) |
||||
|
|
||||
|
// Disable password lock
|
||||
|
await app.client.click('[data-e2e=passwordLock_button]') |
||||
|
await waitForExpectedText(app, '[data-e2e=modal_title]', 'Disable password lock') |
||||
|
await app.client.setValue('#password', 5) |
||||
|
await app.client.pause(500) |
||||
|
await app.client.keys('Enter') |
||||
|
await waitForExpectedText(app, '[data-e2e=settings_title]', 'Settings') |
||||
|
await app.client.pause(3000) |
||||
|
const UnlockedfileContent = fs.readFileSync(tmpAppJSONPath, 'utf-8') |
||||
|
// Verify in app.json that accounts data are not encrypted
|
||||
|
await expect(UnlockedfileContent).toContain(accountsOperations) |
||||
|
await app.client.pause(1000) |
||||
|
}, |
||||
|
TIMEOUT, |
||||
|
) |
||||
|
}) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,47 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# get app version |
||||
|
ledgerLiveVersion=$(grep version package.json | cut -d : -f 2 | sed -E 's/.*"([^"]*)".*/\1/g') |
||||
|
|
||||
|
# OS settings |
||||
|
if [[ $(uname) == 'Darwin' ]]; then \ |
||||
|
settingsPath=~/Library/Application\ Support/Ledger\ Live/ |
||||
|
appPath="/Applications/Ledger Live.app/Contents/MacOS/Ledger Live" |
||||
|
elif [[ $(uname) == 'Linux' ]]; then \ |
||||
|
settingsPath="$HOME/.config/Ledger Live" |
||||
|
appPath="$HOME/apps/ledger-live-desktop-$ledgerLiveVersion-linux-x86_64.AppImage" |
||||
|
else \ |
||||
|
settingsPath="%AppData\\Roaming\\Ledger Live" |
||||
|
appPath="C:\\Program Files\\Ledger Live\\Ledger Live.exe" |
||||
|
fi |
||||
|
|
||||
|
# clean Ledger Live Application settings directory |
||||
|
rm -rf "$settingsPath" |
||||
|
mkdir "$settingsPath" |
||||
|
|
||||
|
# Copy app.json init file for testing |
||||
|
cp test-e2e/sync/data/empty-app.json "$settingsPath/app.json" |
||||
|
|
||||
|
# Start Ledger Live Desktop app |
||||
|
"$appPath" & |
||||
|
lastPid=$! |
||||
|
|
||||
|
# wait for sync |
||||
|
electron ./test-e2e/sync/wait-sync.js |
||||
|
returnCode=$? |
||||
|
|
||||
|
# kill Ledger Live Desktop process |
||||
|
kill -9 $lastPid |
||||
|
|
||||
|
if [[ $returnCode = 0 ]]; then |
||||
|
echo "[OK] Sync finished" |
||||
|
else |
||||
|
echo "[x] Sync failed" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Copy app.json file to test folder |
||||
|
cp "$settingsPath"/app.json test-e2e/sync/data/actual-app.json |
||||
|
|
||||
|
# compare new app.json with expected_app.json |
||||
|
./node_modules/.bin/jest test-e2e/sync/sync-accounts.spec.js |
@ -0,0 +1,64 @@ |
|||||
|
const pick = require('lodash/pick') |
||||
|
|
||||
|
const ACCOUNTS_FIELDS = [ |
||||
|
'archived', |
||||
|
'freshAddress', |
||||
|
'freshAddressPath', |
||||
|
'id', |
||||
|
'index', |
||||
|
'isSegwit', |
||||
|
'name', |
||||
|
'path', |
||||
|
'xpub', |
||||
|
'operations', |
||||
|
'currencyId', |
||||
|
'unitMagnitude', |
||||
|
'balance', |
||||
|
] |
||||
|
|
||||
|
const OPS_FIELDS = ['id', 'hash', 'accountId', 'type', 'senders', 'recipients', 'value', 'fee'] |
||||
|
|
||||
|
const OP_SORT = (a, b) => { |
||||
|
const aHash = getOpHash(a) |
||||
|
const bHash = getOpHash(b) |
||||
|
if (aHash < bHash) return -1 |
||||
|
if (aHash > bHash) return 1 |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
const ACCOUNT_SORT = (a, b) => { |
||||
|
const aHash = getAccountHash(a) |
||||
|
const bHash = getAccountHash(b) |
||||
|
if (aHash < bHash) return -1 |
||||
|
if (aHash > bHash) return 1 |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
describe('sync accounts', () => { |
||||
|
test('should give the same app.json', () => { |
||||
|
const expected = getSanitized('./data/expected-app.json') |
||||
|
const actual = getSanitized('./data/actual-app.json') |
||||
|
expect(actual).toEqual(expected) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
function getSanitized(filePath) { |
||||
|
const data = require(`${filePath}`) // eslint-disable-line import/no-dynamic-require
|
||||
|
const accounts = data.data.accounts.map(a => a.data) |
||||
|
accounts.sort(ACCOUNT_SORT) |
||||
|
return accounts.map(a => pick(a, ACCOUNTS_FIELDS)).map(a => { |
||||
|
a.operations.sort(OP_SORT) |
||||
|
return { |
||||
|
...a, |
||||
|
operations: a.operations.map(o => pick(o, OPS_FIELDS)), |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function getOpHash(op) { |
||||
|
return `${op.accountId}--${op.hash}--${op.type}` |
||||
|
} |
||||
|
|
||||
|
function getAccountHash(account) { |
||||
|
return `${account.name}` |
||||
|
} |
@ -0,0 +1,52 @@ |
|||||
|
/* eslint-disable no-console */ |
||||
|
|
||||
|
const electron = require('electron') |
||||
|
const fs = require('fs') |
||||
|
const path = require('path') |
||||
|
const moment = require('moment') |
||||
|
|
||||
|
const delay = ms => new Promise(f => setTimeout(f, ms)) |
||||
|
|
||||
|
const MIN_TIME_DIFF = 1 * 1000 * 90 // 1.5 minute
|
||||
|
const PING_INTERVAL = 1 * 1000 // 1 seconds
|
||||
|
|
||||
|
async function waitForSync() { |
||||
|
let MAX_RETRIES = 100 |
||||
|
const userDataDirectory = electron.app.getPath('userData') |
||||
|
const tmpAppJSONPath = path.resolve(userDataDirectory, 'app.json') |
||||
|
const appJSONPath = tmpAppJSONPath.replace('/Electron/', '/Ledger Live/') |
||||
|
|
||||
|
function check() { |
||||
|
const appJSONContent = fs.readFileSync(appJSONPath, 'utf-8') |
||||
|
const appJSONParsed = JSON.parse(appJSONContent) |
||||
|
const mapped = appJSONParsed.data.accounts.map(a => ({ |
||||
|
name: a.data.name, |
||||
|
lastSyncDate: a.data.lastSyncDate, |
||||
|
})) |
||||
|
const now = Date.now() |
||||
|
const areAllSync = mapped.every(account => { |
||||
|
const diff = now - new Date(account.lastSyncDate).getTime() |
||||
|
if (diff <= MIN_TIME_DIFF) return true |
||||
|
console.log( |
||||
|
`[${account.name}] synced ${moment(account.lastSyncDate).fromNow()} (${moment( |
||||
|
account.lastSyncDate, |
||||
|
).format('YYYY-MM-DD HH:mm:ss')})`,
|
||||
|
) |
||||
|
return false |
||||
|
}) |
||||
|
return areAllSync |
||||
|
} |
||||
|
|
||||
|
while (!check()) { |
||||
|
MAX_RETRIES-- |
||||
|
if (!MAX_RETRIES) { |
||||
|
console.log(`x Too much retries. Exitting.`) |
||||
|
process.exit(1) |
||||
|
} |
||||
|
await delay(PING_INTERVAL) |
||||
|
} |
||||
|
|
||||
|
process.exit(0) |
||||
|
} |
||||
|
|
||||
|
waitForSync() |
Loading…
Reference in new issue