Merge Boundary UI OSS branch 'main' at commit 7816772e

This commit is contained in:
github-team-secure-boundary committed 2023-02-23 23:31:21 +00:00
commit 0a7b4d28ba
9 files changed
+461 -1

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# e2e test output
tests/e2e/artifacts
+6 -1
View File
@@ -33,6 +33,10 @@
"pretest": "yarn clean:coverage",
"test": "npm-run-all test:*",
"test:ember": "COVERAGE=true ember exam --test-port 0 --split=4 --parallel=1 --random",
"e2e": "yarn playwright test --config ./tests/e2e/playwright.config.js",
"e2e:dev:chrome": "yarn run e2e --project=chromium --headed",
"e2e:dev:firefox": "yarn run e2e --project=firefox --headed",
"e2e:dev:safari": "yarn run e2e --project=webkit --headed",
"posttest": "ember coverage-merge && yarn clean:coverage"
},
"dependencies": {
@@ -48,6 +52,7 @@
"@faker-js/faker": "^7.3.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@playwright/test": "^1.30.0",
"babel-loader": "^8.2.5",
"broccoli-asset-rev": "^3.0.0",
"clipboard": "^2.0.8",
@@ -91,10 +96,10 @@
"qunit": "^2.19.1",
"qunit-dom": "^2.0.0",
"rose": "*",
"sinon": "^14.0.0",
"stylelint": "^14.14.0",
"stylelint-config-prettier-scss": "^0.0.1",
"stylelint-config-standard-scss": "^6.0.0",
"sinon": "^14.0.0",
"webpack": "^5.72.1"
},
"engines": {
+49
View File
@@ -0,0 +1,49 @@
# Admin UI e2e tests
This test suite tests the Boundary Admin UI in an end-to-end setting. It was designed to be run in a
variety of environments as long as the appropriate environment variables are set. You can choose to
spin up your own infrastructure or use [Enos](https://github.com/hashicorp/boundary/tree/main/enos)
to generate it for you.
The test suite uses the [Playwright](https://playwright.dev/) framework.
## Getting Started
Setup...
```bash
cd boundary-ui/ui/admin
yarn install
npx playwright install # this installs the browsers used by Playwright
```
Ensure the `boundary` CLI is available on the path. You can validate this by making sure this
command returns something.
```bash
which boundary
```
Run tests...
```bash
export BOUNDARY_ADDR=
export E2E_PASSWORD_ADMIN_LOGIN_NAME=
export E2E_PASSWORD_ADMIN_PASSWORD=
export E2E_PASSWORD_AUTH_METHOD_ID=
... # Tests may have additional variables that it needs
cd boundary-ui/ui/admin
yarn run e2e
```
Here are some additional commands to assist with debugging.
```bash
PWDEBUG=console yarn playwright test --headed --config ./tests/e2e/playwright.config.js login.spec.js
PWDEBUG=console yarn playwright test --headed --config ./tests/e2e/playwright.config.js login.spec.js:13 --debug
PWDEBUG=console yarn playwright test --headed --config ./tests/e2e/playwright.config.js login.spec.js --debug
```
## Developing Tests
It is recommended to use locators that resemble how users interact with the application. `getByRole`
should be the most prioritized locator as this is the closest way to how users and
accessibility features perceive the page. `getByLabel` should be used for form fields.
Additional information can be found [here](https://playwright.dev/docs/locators#locating-elements)
and [here](https://testing-library.com/docs/queries/about/#priority).
+28
View File
@@ -0,0 +1,28 @@
/* eslint-disable no-undef */
const { chromium } = require('@playwright/test');
const { checkEnv, authenticatedState } = require('./helpers/general');
module.exports = async () => {
await checkEnv([
'BOUNDARY_ADDR',
'E2E_PASSWORD_ADMIN_LOGIN_NAME',
'E2E_PASSWORD_ADMIN_PASSWORD',
'E2E_PASSWORD_AUTH_METHOD_ID',
]);
// Log in and save the authenticated state to reuse in tests
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.env.BOUNDARY_ADDR);
await page
.getByLabel('Login Name')
.fill(process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME);
await page
.getByLabel('Password')
.fill(process.env.E2E_PASSWORD_ADMIN_PASSWORD);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.getByRole('navigation', { name: 'General' }).waitFor();
await page.getByText(process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME).waitFor();
await page.context().storageState({ path: authenticatedState });
await browser.close();
};
@@ -0,0 +1,33 @@
/* eslint-disable no-undef */
const { execSync } = require('child_process');
/**
* Checks that the boundary cli is available
*/
exports.checkBoundaryCli = async () => {
try {
execSync('which boundary');
} catch (e) {
throw new Error('boundary does not exist on the path');
}
};
/**
* Uses the boundary CLI to authenticate to the specified Boundary instance
*/
exports.authenticateBoundaryCli = async () => {
try {
execSync(
'boundary authenticate password' +
' -addr=' +
process.env.BOUNDARY_ADDR +
' -auth-method-id=' +
process.env.E2E_PASSWORD_AUTH_METHOD_ID +
' -login-name=' +
process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME +
' -password=env://E2E_PASSWORD_ADMIN_PASSWORD'
);
} catch (e) {
console.log(`${e.stderr}`);
}
};
+187
View File
@@ -0,0 +1,187 @@
/* eslint-disable no-undef */
const { expect } = require('@playwright/test');
const { nanoid } = require('nanoid');
/**
* Uses the UI to create a new organization
* @param {Page} page Playwright page object
* @returns Name of the organization
*/
exports.createNewOrg = async (page) => {
const orgName = 'Org ' + nanoid();
await page
.getByRole('navigation', { name: 'General' })
.getByRole('link', { name: 'Orgs' })
.click();
await page.getByRole('link', { name: 'New Org' }).click();
await page.getByLabel('Name').fill(orgName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByRole('link', { name: orgName })
).toBeVisible();
return orgName;
};
/**
* Uses the UI to create a new project. Assumes you have selected the desired org.
* @param {Page} page Playwright page object
* @returns Name of the project
*/
exports.createNewProject = async (page) => {
const projectName = 'Project ' + nanoid();
await page
.getByRole('navigation', { name: 'General' })
.getByRole('link', { name: 'Projects' })
.click();
await page.getByRole('link', { name: 'New' }).click();
await page.getByLabel('Name').fill(projectName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByRole('link', { name: projectName })
).toBeVisible();
return projectName;
};
/**
* Uses the UI to create a new host catalog. Assumes you have selected the desired project.
* @param {Page} page Playwright page object
* @returns Name of the host catalog
*/
exports.createNewHostCatalog = async (page) => {
const hostCatalogName = 'Host Catalog ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await page.getByRole('link', { name: 'New' }).click();
await page.getByLabel('Name').fill(hostCatalogName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByRole('link', { name: hostCatalogName })
).toBeVisible();
return hostCatalogName;
};
/**
* Uses the UI to create a new host set. Assumes you have just created a new host catalog.
* @param {Page} page Playwright page object
* @returns Name of the host set
*/
exports.createNewHostSet = async (page) => {
const hostSetName = 'Host Set ' + nanoid();
await page.getByRole('link', { name: 'Host Sets' }).click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name').fill(hostSetName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByRole('link', { name: hostSetName })
).toBeVisible();
return hostSetName;
};
/**
* Uses the UI to create a new host in a host set. Assumes you have just created a new host set.
* @param {Page} page Playwright page object
* @returns Name of the host
*/
exports.createNewHostInHostSet = async (page) => {
const hostName = 'Host ' + nanoid();
await page.getByText('Manage').click();
await page.getByRole('link', { name: 'Create and Add Host' }).click();
await page.getByLabel('Name').fill(hostName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Address').fill(process.env.E2E_TARGET_IP);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(page.getByRole('link', { name: hostName })).toBeVisible();
return hostName;
};
/**
* Uses the UI to create a new target. Assumes you have selected the desired project.
* @param {Page} page Playwright page object
* @returns Name of the target
*/
exports.createNewTarget = async (page) => {
const targetName = 'Target ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: 'New' }).click();
await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Default Port').fill(process.env.E2E_SSH_PORT);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByRole('link', { name: targetName })
).toBeVisible();
return targetName;
};
/**
* Uses the UI to add a host source to a target. Assume you have selected the desired target.
* @param {Page} page Playwright page object
* @param {string} hostSourceName Name of host source that will be attached to the target
*/
exports.addHostSourceToTarget = async (page, hostSourceName) => {
await page.getByRole('link', { name: 'Host Sources', exact: true }).click();
await page
.getByRole('article')
.getByRole('link', { name: 'Add Host Sources', exact: true })
.click();
await page
.getByRole('cell', { name: hostSourceName })
.locator('..')
.getByRole('checkbox')
.click({ force: true });
await page.getByRole('button', { name: 'Add Host Sources' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(page.getByRole('link', { name: hostSourceName })).toBeVisible();
};
+19
View File
@@ -0,0 +1,19 @@
/* eslint-disable no-undef */
exports.authenticatedState = './tests/e2e/artifacts/authenticated-state.json';
/**
* Checks that the provided environment variables are set
* @param {string[]} envs List of environment variables
*/
exports.checkEnv = async (envs) => {
let missing = [];
for (let env in envs) {
if (!process.env[envs[env]]) {
missing.push(envs[env]);
}
}
if (missing.length > 0) {
throw new Error('Missing Environment Variables -- ' + missing);
}
};
+31
View File
@@ -0,0 +1,31 @@
/* eslint-disable no-undef */
const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
globalSetup: require.resolve('./global-setup'),
outputDir: './artifacts/test-failures',
timeout: 45000, // Timeout is shared between all tests
workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI
use: {
baseURL: process.env.BOUNDARY_ADDR,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
};
module.exports = config;
+106
View File
@@ -0,0 +1,106 @@
/* eslint-disable no-undef */
const { test, expect } = require('@playwright/test');
const { exec, execSync } = require('child_process');
const { checkEnv, authenticatedState } = require('../helpers/general');
const {
authenticateBoundaryCli,
checkBoundaryCli,
} = require('../helpers/boundary-cli');
const {
createNewOrg,
createNewProject,
createNewHostCatalog,
createNewHostSet,
createNewHostInHostSet,
createNewTarget,
addHostSourceToTarget,
} = require('../helpers/boundary-ui');
test.use({ storageState: authenticatedState });
test.beforeAll(async () => {
await checkEnv([
'E2E_TARGET_IP',
'E2E_SSH_USER',
'E2E_SSH_KEY_PATH',
'E2E_SSH_PORT',
]);
await checkBoundaryCli();
});
test('Verify session created then cancel the session', async ({ page }) => {
await page.goto('/');
var connect;
try {
const orgName = await createNewOrg(page);
const projectName = await createNewProject(page);
await createNewHostCatalog(page);
const hostSetName = await createNewHostSet(page);
await createNewHostInHostSet(page);
const targetName = await createNewTarget(page);
await addHostSourceToTarget(page, hostSetName);
await authenticateBoundaryCli();
const orgs = JSON.parse(execSync('boundary scopes list -format json'));
const org = orgs.items.filter((obj) => obj.name == orgName)[0];
const projects = JSON.parse(
execSync('boundary scopes list -format json -scope-id ' + org.id)
);
const project = projects.items.filter((obj) => obj.name == projectName)[0];
const targets = JSON.parse(
execSync('boundary targets list -format json -scope-id ' + project.id)
);
const target = targets.items.filter((obj) => obj.name == targetName)[0];
connect = exec(
'boundary connect' +
' -target-id=' +
target.id +
' -exec /usr/bin/ssh --' +
' -l ' +
process.env.E2E_SSH_USER +
' -i ' +
process.env.E2E_SSH_KEY_PATH +
' -o UserKnownHostsFile=/dev/null' +
' -o StrictHostKeyChecking=no' +
' -o IdentitiesOnly=yes' + // forces the use of the provided key
' -p {{boundary.port}}' +
' {{boundary.ip}}'
);
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Sessions' })
.click();
let i = 0;
let sessionIsVisible = false;
do {
i = i + 1;
sessionIsVisible = await page
.getByRole('cell', { name: targetName })
.isVisible();
if (sessionIsVisible) {
break;
}
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByRole('button', { name: 'Refresh' })).toBeEnabled();
} while (i < 5);
if (!sessionIsVisible) {
throw new Error('Session is not visible');
}
await page
.getByRole('cell', { name: targetName })
.locator('..')
.getByRole('button', { name: 'Cancel' })
.click();
} finally {
// End `boundary connect` process
if (connect) {
connect.kill('SIGTERM');
}
}
});