Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions KeychainExample/e2e/testCases/accessControlTest.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { by, device, element, expect, waitFor } from 'detox';
import { matchLoadInfo } from '../utils/matchLoadInfo';
import { by, device, element, expect } from 'detox';
import {
waitForAuthValidity,
enterBiometrics,
enterPasscode,
} from '../utils/authHelpers';
import {
expectCredentialsLoadedMessage,
expectCredentialsSavedMessage,
expectCredentialsResetMessage,
} from '../utils/statusMessageHelpers';

describe('Access Control', () => {
beforeEach(async () => {
Expand All @@ -28,16 +32,14 @@ describe('Access Control', () => {
await enterPasscode();
// Hide keyboard if open
await element(by.text('Keychain Example')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(4000);
await expectCredentialsSavedMessage();

await waitForAuthValidity();
await element(by.text('Load')).tap();
await enterPasscode();
// Hide keyboard if open
await element(by.text('Keychain Example')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernamePasscode',
'testPasswordPasscode',
'KeystoreAESGCM'
Expand Down Expand Up @@ -69,15 +71,16 @@ describe('Access Control', () => {
await element(by.text('Save')).tap();
await enterBiometrics();

await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();

await waitForAuthValidity();
await element(by.text('Load')).tap();
await enterBiometrics();

await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics');
await expectCredentialsLoadedMessage(
'testUsernameBiometrics',
'testPasswordBiometrics'
);
}
);

Expand All @@ -91,7 +94,10 @@ describe('Access Control', () => {
).toBeVisible();
await element(by.text('Load')).tap();
await enterBiometrics();
await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics');
await expectCredentialsLoadedMessage(
'testUsernameBiometrics',
'testPasswordBiometrics'
);
}
);

Expand All @@ -112,11 +118,12 @@ describe('Access Control', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo('testUsernameAny', 'testPasswordAny');
await expectCredentialsLoadedMessage(
'testUsernameAny',
'testPasswordAny'
);
}
);

Expand All @@ -129,7 +136,10 @@ describe('Access Control', () => {
element(by.text('hasGenericPassword: true'))
).toBeVisible();
await element(by.text('Load')).tap();
await matchLoadInfo('testUsernameAny', 'testPasswordAny');
await expectCredentialsLoadedMessage(
'testUsernameAny',
'testPasswordAny'
);
}
);
});
Expand All @@ -139,6 +149,6 @@ describe('Access Control', () => {
// Hide keyboard

await element(by.text('Reset')).tap();
await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible();
await expectCredentialsResetMessage();
});
});
28 changes: 13 additions & 15 deletions KeychainExample/e2e/testCases/securityLevelTest.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { by, device, element, expect, waitFor } from 'detox';
import { matchLoadInfo } from '../utils/matchLoadInfo';
import { by, element, expect, device } from 'detox';
import {
expectCredentialsLoadedMessage,
expectCredentialsSavedMessage,
expectCredentialsResetMessage,
} from '../utils/statusMessageHelpers';

describe(':android:Security Level', () => {
beforeEach(async () => {
Expand All @@ -19,11 +23,9 @@ describe(':android:Security Level', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameAny',
'testPasswordAny',
undefined,
Expand All @@ -46,11 +48,9 @@ describe(':android:Security Level', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameSoftware',
'testPasswordSoftware',
undefined,
Expand All @@ -74,11 +74,9 @@ describe(':android:Security Level', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameHardware',
'testPasswordHardware',
undefined,
Expand All @@ -93,6 +91,6 @@ describe(':android:Security Level', () => {
// Hide keyboard

await element(by.text('Reset')).tap();
await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible();
await expectCredentialsResetMessage();
});
});
38 changes: 16 additions & 22 deletions KeychainExample/e2e/testCases/storageTypesTest.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { by, device, element, expect, waitFor } from 'detox';
import { matchLoadInfo } from '../utils/matchLoadInfo';
import { by, element, expect, device } from 'detox';
import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers';

import {
expectCredentialsLoadedMessage,
expectCredentialsSavedMessage,
expectCredentialsResetMessage,
} from '../utils/statusMessageHelpers';

describe(':android:Storage Types', () => {
beforeEach(async () => {
await device.launchApp({ newInstance: true });
Expand All @@ -20,11 +25,9 @@ describe(':android:Storage Types', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameAESCBC',
'testPasswordAESCBC',
'KeystoreAESCBC',
Expand All @@ -46,13 +49,11 @@ describe(':android:Storage Types', () => {
await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await enterBiometrics();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await waitForAuthValidity();
await element(by.text('Load')).tap();
await enterBiometrics();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameAESGCM',
'testPasswordAESGCM',
'KeystoreAESGCM',
Expand All @@ -79,11 +80,9 @@ describe(':android:Storage Types', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(3000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameAESGCMNoAuth',
'testPasswordAESGCMNoAuth',
'KeystoreAESGCM_NoAuth',
Expand All @@ -105,15 +104,10 @@ describe(':android:Storage Types', () => {

await expect(element(by.text('Save'))).toBeVisible();
await element(by.text('Save')).tap();
await waitFor(element(by.text(/^Credentials saved! .*$/)))
.toExist()
.withTimeout(5000);
await expectCredentialsSavedMessage();
await element(by.text('Load')).tap();
await enterBiometrics();
await waitFor(element(by.text(/^Credentials loaded! .*$/)))
.toExist()
.withTimeout(5000);
await matchLoadInfo(
await expectCredentialsLoadedMessage(
'testUsernameRSA',
'testPasswordRSA',
'KeystoreRSAECB',
Expand All @@ -127,6 +121,6 @@ describe(':android:Storage Types', () => {
// Hide keyboard

await element(by.text('Reset')).tap();
await expect(element(by.text(/^Credentials Reset!$/))).toExist();
await expectCredentialsResetMessage();
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { device } from 'detox';
import cp from 'child_process';
import { device } from 'detox';

// Wait for 5 seconds to ensure auth validity period has expired
export const waitForAuthValidity = async () => {
await new Promise((resolve) => setTimeout(resolve, 5500)); // Added 500ms buffer
await new Promise((resolve) => setTimeout(resolve, 5500)); // buffer needed for auth validity period
};

export const enterBiometrics = async () => {
Expand All @@ -12,16 +12,14 @@ export const enterBiometrics = async () => {
if (device.getPlatform() === 'android') {
await new Promise((resolve) => setTimeout(resolve, 1000));
cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']);
await new Promise((resolve) => setTimeout(resolve, 500));
}
};

export const enterPasscode = async () => {
if (device.getPlatform() === 'android') {
await new Promise((resolve) => setTimeout(resolve, 1500));
await new Promise((resolve) => setTimeout(resolve, 1000));
cp.spawnSync('adb', ['shell', 'input', 'text', '1111']);
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 1000));
cp.spawnSync('adb', ['shell', 'input', 'keyevent', '66']);
await new Promise((resolve) => setTimeout(resolve, 1500));
}
};
36 changes: 36 additions & 0 deletions KeychainExample/e2e/utils/detoxHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { by, element, waitFor, expect } from 'detox';

async function retry<T>(
operation: () => Promise<T>,
maxAttempts: number = 3,
delayMs: number = 1000
): Promise<T> {
let attempts = 0;

while (attempts < maxAttempts) {
try {
return await operation();
} catch (error) {
attempts++;
if (attempts === maxAttempts) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
throw new Error('Unreachable code');
}

export async function expectRegexText(regex: RegExp, timeout?: number) {
try {
return await retry(async () =>
timeout
? waitFor(element(by.text(regex)))
.toBeVisible()
.withTimeout(timeout)
: expect(element(by.text(regex))).toBeVisible()
);
} catch (error) {
throw new Error(`Failed to find text matching ${regex}: ${error}`);
}
}
26 changes: 0 additions & 26 deletions KeychainExample/e2e/utils/matchLoadInfo.ts

This file was deleted.

49 changes: 49 additions & 0 deletions KeychainExample/e2e/utils/statusMessageHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expectRegexText } from './detoxHelpers';

const TIMEOUT = 10000;

function buildLoadedCredentialsRegex(
username: string,
password: string,
storage?: string,
service?: string
): RegExp {
let pattern = '^Credentials loaded! .*';
// Conditionally add storage if provided.
if (storage) {
pattern += `"storage":"${storage}",`;
}
// Always add password and username.
pattern += `"password":"${password}","username":"${username}"`;
// Conditionally add service if provided.
if (service) {
pattern += `,"service":"${service}"`;
}
pattern += '.*$';
return new RegExp(pattern);
}

export async function expectCredentialsSavedMessage() {
const regex = /^Credentials saved! .*$/;
await expectRegexText(regex, TIMEOUT);
}

export async function expectCredentialsResetMessage() {
const regex = /^Credentials Reset!$/;
await expectRegexText(regex, TIMEOUT);
}

export async function expectCredentialsLoadedMessage(
username: string,
password: string,
storage?: string,
service?: string
) {
const regex = buildLoadedCredentialsRegex(
username,
password,
storage,
service
);
await expectRegexText(regex, TIMEOUT);
}
Loading