diff --git a/tests/e2e_tests/integration/00-setup.spec.ts b/tests/e2e_tests/integration/00-setup.spec.ts index 65fe0030aa..ae962ad090 100644 --- a/tests/e2e_tests/integration/00-setup.spec.ts +++ b/tests/e2e_tests/integration/00-setup.spec.ts @@ -99,7 +99,7 @@ test.describe('Test setup', () => { const { token: JWT, userId } = await login(username, password, baseUrl); const domain = baseUrlToDomain(baseUrl); context = await prepareCookies(context, domain, userId); - context.addInitScript(token => { + await context.addInitScript(token => { window.localStorage.setItem('JWT', JSON.stringify({ token })); window.localStorage.setItem(`onboardingComplete`, 'true'); }, JWT); diff --git a/tests/e2e_tests/integration/03-files.spec.ts b/tests/e2e_tests/integration/03-files.spec.ts index e455d592b9..445e963a82 100644 --- a/tests/e2e_tests/integration/03-files.spec.ts +++ b/tests/e2e_tests/integration/03-files.spec.ts @@ -19,28 +19,48 @@ import https from 'https'; import md5 from 'md5'; import test, { expect } from '../fixtures/fixtures'; -import { selectors, storagePath, timeouts } from '../utils/constants'; +import { getTokenFromStorage, tagRelease } from '../utils/commands'; +import { releaseTag, selectors, storagePath, timeouts } from '../utils/constants'; dayjs.extend(isBetween); -const releaseTag = 'someTag'; - test.describe('Files', () => { const fileName = 'mender-demo-artifact.mender'; test.use({ storageState: storagePath }); - test('allows file uploads', async ({ loggedInPage: page }) => { + test.beforeEach(async ({ loggedInPage: page }) => { await page.click(`.leftNav :text('Releases')`); - // create an artifact to download first - await page.click(`button:has-text('Upload')`); + }); + + test('allows file uploads', async ({ loggedInPage: page }) => { + const uploadButton = await page.getByRole('button', { name: /upload/i }); + await uploadButton.click(); await page.locator('.MuiDialog-paper .dropzone input').setInputFiles(`fixtures/${fileName}`); await page.click(`.MuiDialog-paper button:has-text('Upload')`); - // give some extra time for the upload - await page.waitForTimeout(timeouts.fiveSeconds); + await page.waitForSelector('text=/last modified/i'); }); - test('allows release notes manipulation', async ({ loggedInPage: page }) => { + test('allows artifact generation', async ({ baseUrl, loggedInPage: page }) => { + const releaseName = 'terminalImage'; + const uploadButton = await page.getByRole('button', { name: /upload/i }); + await uploadButton.click(); + await page.locator('.MuiDialog-paper .dropzone input').setInputFiles(`fixtures/terminalContent.png`); + await page.getByPlaceholder(/installed-by-single-file/i).fill(`/usr/src`); + const deviceTypeInput = await page.getByLabel(/Release name/i); + await deviceTypeInput.clear(); + await deviceTypeInput.fill(releaseName); + await page.getByLabel(/Device types/i).fill(`all-of-them,`); + await page.getByRole('button', { name: /next/i }).click(); + await page.getByRole('button', { name: /upload artifact/i }).click(); + await page.waitForSelector('text=1-2 of 2'); + const token = await getTokenFromStorage(baseUrl); + await tagRelease(releaseName, 'customRelease', baseUrl, token); + await page.waitForTimeout(timeouts.oneSecond); // some extra time for the release to be tagged in the backend await page.click(`.leftNav :text('Releases')`); + expect(await page.getByText(/customRelease/i)).toBeVisible(); + }); + + test('allows release notes manipulation', async ({ loggedInPage: page }) => { await page.getByText(/demo-artifact/i).click(); expect(await page.getByRole('heading', { name: /Release notes/i }).isVisible()).toBeTruthy(); const hasNotes = await page.getByText('foo notes').isVisible(); @@ -62,7 +82,6 @@ test.describe('Files', () => { }); test('allows release tags manipulation', async ({ baseUrl, loggedInPage: page }) => { - await page.click(`.leftNav :text('Releases')`); const alreadyTagged = await page.getByText('some, tags').isVisible(); test.skip(alreadyTagged, 'looks like the release was tagged already'); await page.getByText(/demo-artifact/i).click(); @@ -87,7 +106,6 @@ test.describe('Files', () => { }); test('allows release tags reset', async ({ loggedInPage: page }) => { - await page.click(`.leftNav :text('Releases')`); await page.getByText(/demo-artifact/i).click(); const theDiv = await page .locator('div') @@ -120,7 +138,6 @@ test.describe('Files', () => { }); test('allows release tags filtering', async ({ loggedInPage: page }) => { - await page.click(`.leftNav :text('Releases')`); expect(await page.getByText(releaseTag.toLowerCase()).isVisible()).toBeTruthy(); await page.getByPlaceholder(/select tags/i).fill('foo,'); const releasesNote = await page.getByText(/There are no Releases*/i); @@ -149,7 +166,6 @@ test.describe('Files', () => { // }) test('allows artifact downloads', async ({ loggedInPage: page }) => { - await page.click(`.leftNav :text('Releases')`); await page.click(`text=/mender-demo-artifact/i`); await page.click('.expandButton'); const downloadButton = await page.getByText(/download artifact/i); diff --git a/tests/e2e_tests/integration/08-rbac.spec.ts b/tests/e2e_tests/integration/08-rbac.spec.ts index 658b079678..16be8155f4 100644 --- a/tests/e2e_tests/integration/08-rbac.spec.ts +++ b/tests/e2e_tests/integration/08-rbac.spec.ts @@ -13,10 +13,16 @@ // limitations under the License. import test, { expect } from '../fixtures/fixtures'; import { isLoggedIn, processLoginForm } from '../utils/commands'; -import { selectors, timeouts } from '../utils/constants'; +import { releaseTag, selectors, timeouts } from '../utils/constants'; + +const releaseRoles = [ + { name: 'test-releases-role', permissions: ['Read'], tag: undefined }, + { name: `test-manage-${releaseTag}-role`, permissions: ['Manage'], tag: releaseTag }, + { name: `test-ro-${releaseTag}-role`, permissions: ['Read'], tag: releaseTag } +]; test.describe('RBAC functionality', () => { - test('allows access to user management', async ({ baseUrl, loggedInPage: page }) => { + test.beforeEach(async ({ baseUrl, loggedInPage: page }) => { await page.goto(`${baseUrl}ui/settings`); await page.waitForSelector('text=/Global settings/i'); await page.click('text=/user management/i'); @@ -27,21 +33,21 @@ test.describe('RBAC functionality', () => { await page.goto(`${baseUrl}ui/help`); await page.goto(`${baseUrl}ui/settings/user-management`); } + }); + + test('allows access to user management', async ({ loggedInPage: page }) => { await page.getByRole('button', { name: /new user/i }).isVisible(); }); - test('allows role creation for static groups', async ({ baseUrl, environment, loggedInPage: page }) => { + + test('allows role creation for static groups', async ({ environment, loggedInPage: page }) => { test.skip(!['enterprise', 'staging'].includes(environment)); - // test.use({ storageState: storagePath }); - await page.goto(`${baseUrl}ui/settings`); - await page.waitForSelector('text=/Global settings/i'); await page.click('text=/roles/i'); - await page.waitForSelector('text=Add a role'); await page.click('text=Add a role'); let nameInput = await page.locator('label:has-text("name") >> ..'); const dialog = await nameInput.locator('.. >> .. >> ..'); nameInput = await nameInput.locator('input'); await nameInput.click(); - await nameInput.fill('testRole'); + await nameInput.fill('test-groups-role'); await nameInput.press('Tab'); await dialog.locator('#role-description').fill('some description'); await dialog.locator('text=Search groups​').click({ force: true }); @@ -51,60 +57,121 @@ test.describe('RBAC functionality', () => { await dialog.locator('text=Select​').nth(1).click({ force: true }); await page.locator('text=Configure').click(); await page.press('body', 'Escape'); - - await page.waitForTimeout(timeouts.oneSecond); - await dialog.locator('text=Search release tags​').click({ force: true }); - await page.locator('li[role="option"]:has-text("All releases")').click({ force: true }); - await dialog.locator('text=Select​').first().click({ force: true }); - await page.locator('li[role="option"]:has-text("Read")').click(); - await page.press('body', 'Escape'); - await dialog.locator('text=Submit').scrollIntoViewIfNeeded(); - await dialog.locator('text=Submit').click(); + await dialog.getByRole('button', { name: /submit/i }).scrollIntoViewIfNeeded(); + await dialog.getByRole('button', { name: /submit/i }).click(); }); - test('allows user creation', async ({ baseUrl, environment, loggedInPage: page, password, username }) => { - await page.goto(`${baseUrl}ui/settings/user-management`); - await page.getByRole('button', { name: /new user/i }).click(); - await page.getByPlaceholder(/email/i).click(); - await page.getByPlaceholder(/email/i).fill(`limited-${username}`); - await page.getByPlaceholder(/Password/i).click(); - await page.getByPlaceholder(/Password/i).fill(password); - if (['enterprise', 'staging'].includes(environment)) { - await page.getByRole('combobox', { name: /admin/i }).click(); - // first we need to deselect the default admin role - await page.getByRole('option', { name: 'Admin' }).click(); - await page.getByRole('option', { name: 'testRole' }).click(); + test('allows role creation for release tags', async ({ environment, loggedInPage: page }) => { + test.skip(!['enterprise', 'staging'].includes(environment)); + await page.click('text=/roles/i'); + for (const { name, permissions, tag } of releaseRoles) { + await page.click('text=Add a role'); + let nameInput = await page.locator('label:has-text("name") >> ..'); + const dialog = await nameInput.locator('.. >> .. >> ..'); + nameInput = await nameInput.locator('input'); + await nameInput.click(); + await nameInput.fill(name); + await nameInput.press('Tab'); + await dialog.locator('#role-description').fill('some description'); + // we need to check the entire page here, since the selection list is rendered in a portal, so likely outside + // of the dialog tree + await dialog.locator('text=Search release tags​').click({ force: true }); + if (tag) { + await page.locator(`li[role="option"]:has-text("${tag}")`).click(); + } else { + await page.locator(`li[role="option"]:has-text("All releases")`).click({ force: true }); + } + await dialog.locator('text=Select​').first().click({ force: true }); + await Promise.all(permissions.map(async permission => await page.locator(`li[role="option"]:has-text("${permission}")`).click())); await page.press('body', 'Escape'); + await dialog.getByRole('button', { name: /submit/i }).scrollIntoViewIfNeeded(); + await dialog.getByRole('button', { name: /submit/i }).click(); + await page.waitForSelector('text=The role was created successfully.'); } - await page.click(`text=/Create user/i`); - await page.waitForSelector('text=The user was created successfully.'); }); - test('can log in to a newly created user', async ({ baseUrl, page, environment, password, username }) => { - await page.goto(`${baseUrl}ui/`); - // enter valid username and password of the new user - await processLoginForm({ username, password, environment, page }); - await isLoggedIn(page); + test('allows user creation', async ({ environment, loggedInPage: page, password, username }) => { + const userCreations = [ + { user: `limited-${username}`, role: 'test-groups-role' }, + { user: `limited-ro-releases-${username}`, role: releaseRoles[0].name }, + { user: `limited-manage-${releaseTag}-${username}`, role: releaseRoles[1].name }, + { user: `limited-ro-${releaseTag}-${username}`, role: releaseRoles[2].name } + ]; + for (const { user, role } of userCreations) { + await page.getByRole('button', { name: /new user/i }).click(); + await page.getByPlaceholder(/email/i).click(); + await page.getByPlaceholder(/email/i).fill(user); + await page.getByPlaceholder(/Password/i).click(); + await page.getByPlaceholder(/Password/i).fill(password); + if (['enterprise', 'staging'].includes(environment)) { + await page.getByRole('combobox', { name: /admin/i }).click(); + // first we need to deselect the default admin role + await page.getByRole('option', { name: 'Admin' }).click(); + await page.getByRole('option', { name: role }).click(); + await page.press('body', 'Escape'); + } + await page.click(`text=/Create user/i`); + await page.waitForSelector('text=The user was created successfully.'); + } }); - test('has working RBAC limitations', async ({ baseUrl, environment, page, password, username }) => { + test('has working RBAC groups limitations', async ({ baseUrl, browser, environment, password, username }) => { test.skip(!['enterprise', 'staging'].includes(environment)); - await page.goto(`${baseUrl}ui/`); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(baseUrl); // enter valid username and password of the new user await processLoginForm({ username: `limited-${username}`, password, page, environment }); await isLoggedIn(page); - await page.reload(); - const releasesButton = page.getByText(/releases/i); - await releasesButton.waitFor({ timeout: timeouts.tenSeconds }); - await releasesButton.click(); - - // the created role doesn't have permission to upload artifacts, so the button shouldn't be visible - expect(await page.isVisible(`css=button >> text=Upload`)).toBeFalsy(); - await page.click(`.leftNav :text('Devices')`); await page.click(`${selectors.deviceListItem} div:last-child`); // the created role does have permission to configure devices, so the section should be visible await page.click(`text=/configuration/i`); await page.waitForSelector('text=/Device configuration/i', { timeout: timeouts.tenSeconds }); }); + + test.describe('has working RBAC release limitations', () => { + test('read-only all releases', async ({ baseUrl, browser, environment, password, username }) => { + test.skip(!['enterprise', 'staging'].includes(environment)); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(baseUrl); + // enter valid username and password of the new user + await processLoginForm({ username: `limited-ro-releases-${username}`, password, page, environment }); + await isLoggedIn(page); + await page.getByText(/releases/i).click({ timeout: timeouts.tenSeconds }); + // there should be multiple releases present + expect(await page.getByText('1-2 of 2')).toBeVisible(); + // the created role doesn't have permission to upload artifacts, so the button shouldn't be visible + expect(await page.getByRole('button', { name: /upload/i })).not.toBeVisible(); + }); + test('read-only tagged releases', async ({ baseUrl, browser, environment, password, username }) => { + test.skip(!['enterprise', 'staging'].includes(environment)); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(baseUrl); + // enter valid username and password of the new user + await processLoginForm({ username: `limited-ro-${releaseTag}-${username}`, password, page, environment }); + await isLoggedIn(page); + await page.getByText(/releases/i).click({ timeout: timeouts.tenSeconds }); + // there should be only one release tagged with the releaseTag + expect(await page.getByText('1-1 of 1')).toBeVisible(); + // the created role doesn't have permission to upload artifacts, so the button shouldn't be visible + expect(await page.getByRole('button', { name: /upload/i })).not.toBeVisible(); + }); + test('manage tagged releases', async ({ baseUrl, browser, environment, password, username }) => { + test.skip(!['enterprise', 'staging'].includes(environment)); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(baseUrl); + // enter valid username and password of the new user + await processLoginForm({ username: `limited-manage-${releaseTag}-${username}`, password, page, environment }); + await isLoggedIn(page); + await page.getByText(/releases/i).click({ timeout: timeouts.tenSeconds }); + // there should be only one release tagged with the releaseTag + expect(await page.getByText('1-1 of 1')).toBeVisible(); + // the created role does have permission to upload artifacts, so the button should be visible + expect(await page.getByRole('button', { name: /upload/i })).toBeVisible(); + }); + }); }); diff --git a/tests/e2e_tests/utils/commands.ts b/tests/e2e_tests/utils/commands.ts index dc93b23986..f70d3e501f 100644 --- a/tests/e2e_tests/utils/commands.ts +++ b/tests/e2e_tests/utils/commands.ts @@ -51,9 +51,9 @@ export const getStorageState = location => { }; export const getTokenFromStorage = (baseUrl: string) => { - const domain = baseUrlToDomain(baseUrl); - const origin = getStorageState(storagePath).origins.find(({ origin }) => origin === domain); - const textContent = origin?.localStorage.find(({ name }) => name === 'JWT').value ?? ''; + const originUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; + const origin = getStorageState(storagePath).origins.find(({ origin }) => origin === originUrl); + const textContent = origin?.localStorage.find(({ name }) => name === 'JWT').value; let sessionInfo = { token: '' }; try { sessionInfo = JSON.parse(textContent); @@ -288,3 +288,19 @@ export const compareImages = (expectedPath, actualPath, options = { threshold: 0 const pass = usePercentage ? (numDiffPixels / (width * height)) * 100 < threshold : numDiffPixels < threshold; return { pass, numDiffPixels }; }; + +export const tagRelease = async (releaseName: string, tag: string, baseUrl: string, token: string) => { + const request = await axios.put(`${baseUrl}api/management/v2/deployments/deployments/releases/${releaseName}/tags`, [tag], { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + httpsAgent: new https.Agent({ rejectUnauthorized: false }) + }); + + if (request.status >= 300) { + console.error(`failed to tag release ${releaseName} got status:`, request.status); + throw 'oh no'; + } + return Promise.resolve(); +}; diff --git a/tests/e2e_tests/utils/constants.ts b/tests/e2e_tests/utils/constants.ts index 8fb782603e..8f1688fb49 100644 --- a/tests/e2e_tests/utils/constants.ts +++ b/tests/e2e_tests/utils/constants.ts @@ -29,6 +29,8 @@ export const selectors = { terminalText: '.terminal.xterm textarea' }; +export const releaseTag = 'sometag'; + export const storagePath = 'storage.json'; const oneSecond = 1000;