diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore index 72f0a8515..fc8ad50f7 100644 --- a/e2e-tests/.gitignore +++ b/e2e-tests/.gitignore @@ -2,3 +2,5 @@ admin/artifacts desktop/artifacts playwright-report + +.auth \ No newline at end of file diff --git a/e2e-tests/desktop/fixtures/electronTest.js b/e2e-tests/desktop/fixtures/electronTest.js index 729f656bf..0866dced3 100644 --- a/e2e-tests/desktop/fixtures/electronTest.js +++ b/e2e-tests/desktop/fixtures/electronTest.js @@ -13,7 +13,13 @@ import fs from 'fs'; * @return {string} */ const getExecutablePath = (buildDirectory = 'out') => { - const rootDir = path.resolve('../ui/desktop/electron-app'); + // Using process.cwd() can change depending on where you run the tests so we use the current file location + // __dirname isn't available in ES modules so we need to indirectly get the directory + const rootDir = path.resolve( + import.meta.dirname, + '../../../ui/desktop/electron-app', + ); + // directory where the builds are stored const outDir = path.resolve(rootDir, buildDirectory); // list of files in the out directory diff --git a/e2e-tests/desktop/playwright.config.js b/e2e-tests/desktop/playwright.config.js index f361a087a..726c5da4f 100644 --- a/e2e-tests/desktop/playwright.config.js +++ b/e2e-tests/desktop/playwright.config.js @@ -11,7 +11,6 @@ export default defineConfig({ workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI use: { baseURL: process.env.BOUNDARY_ADDR, - storageState: './admin/artifacts/authenticated-state.json', extraHTTPHeaders: { // This token is set in global-setup.js Authorization: `Bearer ${process.env.E2E_TOKEN}`, diff --git a/e2e-tests/desktop/tests/sessions.spec.js b/e2e-tests/desktop/tests/sessions.spec.js new file mode 100644 index 000000000..5692774d9 --- /dev/null +++ b/e2e-tests/desktop/tests/sessions.spec.js @@ -0,0 +1,251 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '../fixtures/baseTest.js'; +import * as boundaryHttp from '../../helpers/boundary-http.js'; + +let org; +let targetWithHost; +let sshTarget; +let sshTarget2; +let credential; + +test.beforeEach( + async ({ request, targetAddress, targetPort, sshUser, sshKeyPath }) => { + org = await boundaryHttp.createOrg(request); + const project = await boundaryHttp.createProject(request, org.id); + + // Create host set + const hostCatalog = await boundaryHttp.createStaticHostCatalog( + request, + project.id, + ); + const host = await boundaryHttp.createHost(request, { + hostCatalogId: hostCatalog.id, + address: targetAddress, + }); + + const hostSet = await boundaryHttp.createHostSet(request, hostCatalog.id); + await boundaryHttp.addHostToHostSet(request, { + hostSet, + hostIds: [host.id], + }); + + // Create a static credential store and key pair credential + const credentialStore = await boundaryHttp.createStaticCredentialStore( + request, + project.id, + ); + credential = await boundaryHttp.createStaticCredentialKeyPair(request, { + credentialStoreId: credentialStore.id, + username: sshUser, + sshKeyPath, + }); + + // Create tcp target with host set and 1 host + targetWithHost = await boundaryHttp.createTarget(request, { + scopeId: project.id, + type: 'tcp', + port: targetPort, + }); + targetWithHost = await boundaryHttp.addHostSource(request, { + target: targetWithHost, + hostSourceIds: [hostSet.id], + }); + targetWithHost = await boundaryHttp.addBrokeredCredentials(request, { + target: targetWithHost, + credentialIds: [credential.id], + }); + + // Create SSH targets with address + sshTarget = await boundaryHttp.createTarget(request, { + scopeId: project.id, + type: 'ssh', + port: targetPort, + address: targetAddress, + }); + sshTarget = await boundaryHttp.addInjectedCredentials(request, { + target: sshTarget, + credentialIds: [credential.id], + }); + + sshTarget2 = await boundaryHttp.createTarget(request, { + scopeId: project.id, + type: 'ssh', + port: targetPort, + address: targetAddress, + }); + sshTarget2 = await boundaryHttp.addInjectedCredentials(request, { + target: sshTarget2, + credentialIds: [credential.id], + }); + }, +); + +test.afterEach(async ({ request }) => { + if (org) { + await boundaryHttp.deleteOrg(request, org.id); + } +}); + +test.describe('Sessions tests', async () => { + test('Establishes two different sessions and cancels them', async ({ + authedPage, + }) => { + // Connect to two targets + await authedPage + .getByRole('row', { name: sshTarget.name }) + .getByRole('link', { name: 'Connect' }) + .click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + await authedPage.getByRole('link', { name: 'Targets' }).click(); + + await authedPage + .getByRole('row', { name: targetWithHost.name }) + .getByRole('link', { name: 'Connect' }) + .click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + await authedPage.getByRole('link', { name: 'Sessions' }).click(); + + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByText('Pending'), + ).toHaveCount(2); + + // Cancel both sessions + for (const cancel of await authedPage.getByLabel('Cancel').all()) { + await cancel.click(); + } + + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByText('Canceling'), + ).toHaveCount(2); + }); +}); + +test.describe('Filtering sessions tests', async () => { + test.beforeEach(async ({ authedPage }) => { + // Connect to three targets + await authedPage + .getByRole('row', { name: sshTarget.name }) + .getByRole('link', { name: 'Connect' }) + .click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + await authedPage.getByRole('link', { name: 'Targets' }).click(); + + await authedPage + .getByRole('row', { name: sshTarget2.name }) + .getByRole('link', { name: 'Connect' }) + .click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + await authedPage.getByRole('link', { name: 'Targets' }).click(); + + await authedPage + .getByRole('row', { name: targetWithHost.name }) + .getByRole('link', { name: 'Connect' }) + .click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + await authedPage.getByRole('link', { name: 'Sessions' }).click(); + + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByRole('cell', { name: 'Pending' }), + ).toHaveCount(3); + }); + + test.afterEach(async ({ authedPage }) => { + await authedPage.getByRole('button', { name: 'Clear Filters' }).click(); + await expect( + authedPage.getByRole('button', { name: 'Clear Filters' }), + ).toBeHidden(); + for (const cancel of await authedPage.getByLabel('Cancel').all()) { + await cancel.click(); + } + + await expect(authedPage.getByLabel('Cancel')).toHaveCount(0); + }); + + test('Filters by target', async ({ authedPage }) => { + await authedPage.getByRole('button', { name: 'Target' }).click(); + await authedPage.getByLabel('Narrow results').fill(targetWithHost.name); + await expect(authedPage.getByRole('checkbox')).toHaveCount(1); + await authedPage.getByLabel(targetWithHost.name).check(); + await authedPage.getByRole('button', { name: 'Apply' }).click(); + + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByRole('cell', { name: 'Pending' }), + ).toHaveCount(1); + await expect( + authedPage.getByRole('cell', { name: targetWithHost.name }), + ).toBeVisible(); + }); + + test('Filters by status', async ({ authedPage }) => { + await authedPage + .getByRole('row', { name: sshTarget.name }) + .getByRole('link') + .first() + .click(); + await authedPage.getByRole('tab', { name: 'Shell' }).click(); + await authedPage.waitForTimeout(3000); + await authedPage.getByRole('link', { name: 'Sessions' }).click(); + + await authedPage.getByRole('button', { name: 'Clear Filters' }).click(); + await expect( + authedPage.getByRole('button', { name: 'Clear Filters' }), + ).toBeHidden(); + await authedPage.getByRole('button', { name: 'Status' }).click(); + await authedPage.getByLabel('Active').check(); + await authedPage.getByRole('button', { name: 'Apply' }).click(); + + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByRole('cell', { name: 'Active' }), + ).toHaveCount(1); + await expect( + authedPage.getByRole('cell', { name: sshTarget.name }), + ).toBeVisible(); + + await authedPage.getByRole('button', { name: 'Clear Filters' }).click(); + await expect( + authedPage.getByRole('button', { name: 'Clear Filters' }), + ).toBeHidden(); + await authedPage.getByRole('button', { name: 'Status' }).click(); + await authedPage.getByLabel('Pending').check(); + await authedPage.getByRole('button', { name: 'Apply' }).click(); + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }) + .getByRole('cell', { name: 'Pending' }), + ).toHaveCount(2); + }); +}); diff --git a/e2e-tests/desktop/tests/targets.spec.js b/e2e-tests/desktop/tests/targets.spec.js index 12f87bf08..b8b532cc7 100644 --- a/e2e-tests/desktop/tests/targets.spec.js +++ b/e2e-tests/desktop/tests/targets.spec.js @@ -4,7 +4,7 @@ */ import { expect, test } from '../fixtures/baseTest.js'; -import * as boundaryHttp from '../../helpers/boundary-http'; +import * as boundaryHttp from '../../helpers/boundary-http.js'; const hostName = 'Host name for test'; let org; @@ -13,7 +13,7 @@ let targetWithTwoHosts; let sshTarget; let credential; -test.beforeAll( +test.beforeEach( async ({ request, targetAddress, targetPort, sshUser, sshKeyPath }) => { org = await boundaryHttp.createOrg(request); const project = await boundaryHttp.createProject(request, org.id); @@ -108,20 +108,20 @@ test.beforeAll( }, ); -test.afterAll(async ({ request }) => { +test.afterEach(async ({ request }) => { if (org) { await boundaryHttp.deleteOrg(request, org.id); } }); -test.describe('Targets test', async () => { +test.describe('Targets tests', async () => { test('Connects to a tcp target with one host', async ({ authedPage }) => { await authedPage.getByRole('link', { name: targetWithHost.name }).click(); await authedPage.getByRole('button', { name: 'Connect' }).click(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/sessions/, - ); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); await expect(authedPage.getByText(credential.name)).toBeVisible(); await expect(authedPage.getByText('username')).toBeVisible(); @@ -153,9 +153,9 @@ test.describe('Targets test', async () => { await authedPage.getByRole('button', { name: 'End Session' }).click(); await expect(authedPage.getByText('Canceled successfully.')).toBeVisible(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/targets$/, - ); + await expect( + authedPage.getByRole('heading', { name: 'Targets' }), + ).toBeVisible(); }); [{ host: 'Quick Connect' }, { host: hostName }].forEach(({ host }) => { @@ -167,14 +167,15 @@ test.describe('Targets test', async () => { .click(); await authedPage.getByRole('button', { name: 'Connect' }).click(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/targets/, - ); + await expect( + authedPage.getByRole('heading', { name: 'Choose a Host' }), + ).toBeVisible(); + await authedPage.getByRole('button', { name: host }).click(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/sessions/, - ); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); await expect(authedPage.getByText(credential.name)).toBeVisible(); await expect(authedPage.getByText('username')).toBeVisible(); @@ -218,9 +219,9 @@ test.describe('Targets test', async () => { await authedPage.getByRole('link', { name: sshTarget.name }).click(); await authedPage.getByRole('button', { name: 'Connect' }).click(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/sessions/, - ); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); await authedPage.getByRole('tab', { name: 'Shell' }).click(); // TODO: Research a better way to test canvas elements for the shell, @@ -228,16 +229,20 @@ test.describe('Targets test', async () => { await authedPage.getByRole('button', { name: 'End Session' }).click(); await expect(authedPage.getByText('Canceled successfully.')).toBeVisible(); - await expect(authedPage).toHaveURL( - /.*\/scopes\/global\/projects\/targets$/, - ); + + await expect( + authedPage.getByRole('heading', { name: 'Targets' }), + ).toBeVisible(); }); test('Searches targets correctly', async ({ authedPage }) => { await authedPage.getByLabel('Search').fill(targetWithHost.name); - // One row is the header - await expect(authedPage.getByRole('row')).toHaveCount(2); + await expect( + authedPage + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }), + ).toHaveCount(1); await expect( authedPage.getByRole('link', { name: targetWithHost.name }), ).toBeVisible(); diff --git a/e2e-tests/global-setup.js b/e2e-tests/global-setup.js index 128e233d4..bf8560c55 100644 --- a/e2e-tests/global-setup.js +++ b/e2e-tests/global-setup.js @@ -6,6 +6,13 @@ import { chromium, test as baseTest } from '@playwright/test'; import { checkEnv } from './helpers/general.js'; import { LoginPage } from './admin/pages/login.js'; +import path from 'path'; +import dotenv from 'dotenv'; + +const __dirname = import.meta.dirname; + +// Import environment variables from .env file if available +dotenv.config({ path: path.resolve(__dirname, './.env') }); async function globalSetup() { await checkEnv([ @@ -42,7 +49,10 @@ const authenticateToBoundary = async () => { export default globalSetup; -export const authenticatedState = './admin/artifacts/authenticated-state.json'; +export const authenticatedState = path.resolve( + __dirname, + './.auth/authenticated-state.json', +); // Centralized location for environment variables used in tests export const test = baseTest.extend({ diff --git a/e2e-tests/package.json b/e2e-tests/package.json index 19195fab8..ae4fc2e05 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -38,7 +38,7 @@ "prettier": "^3.0.0" }, "engines": { - "node": "18.* || 20.*" + "node": "20.* || 22.*" }, "lint-staged": { "*.js": "eslint --fix",