Skip to content

Commit

Permalink
Merge pull request #4420 from mzedel/men-7030
Browse files Browse the repository at this point in the history
MEN-7030 - e2e tests for release tags RBAC
  • Loading branch information
mzedel committed Jun 6, 2024
2 parents 63ef23e + 750869a commit 8b9658d
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 63 deletions.
2 changes: 1 addition & 1 deletion tests/e2e_tests/integration/00-setup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
42 changes: 29 additions & 13 deletions tests/e2e_tests/integration/03-files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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')
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
159 changes: 113 additions & 46 deletions tests/e2e_tests/integration/08-rbac.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 });
Expand All @@ -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();
});
});
});
22 changes: 19 additions & 3 deletions tests/e2e_tests/utils/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
};
2 changes: 2 additions & 0 deletions tests/e2e_tests/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const selectors = {
terminalText: '.terminal.xterm textarea'
};

export const releaseTag = 'sometag';

export const storagePath = 'storage.json';

const oneSecond = 1000;
Expand Down

0 comments on commit 8b9658d

Please sign in to comment.