diff --git a/.gitignore b/.gitignore index f96d10249..6e778d5e1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ playwright/.cache/ .env .auth bin +repoName.txt diff --git a/_playwright-tests/UI/TwoUserRBAC.spec.ts b/_playwright-tests/UI/TwoUserRBAC.spec.ts new file mode 100644 index 000000000..f7dfa738c --- /dev/null +++ b/_playwright-tests/UI/TwoUserRBAC.spec.ts @@ -0,0 +1,81 @@ +import { expect, test } from '@playwright/test'; +import { navigateToRepositories } from '../UI/helpers/navHelpers'; +import { randomName, randomUrl } from '../UI/helpers/repoHelpers'; +import { closePopupsIfExist, getRowByNameOrUrl } from '../UI/helpers/helpers'; +import fs from 'fs'; +import { deleteAllRepos } from '../UI/helpers/deleteRepositories'; + +const repoNamePrefix = 'Repo-RBAC'; +const repoNameFile = 'repoName.txt'; + +// Function to get or generate repo name using file persistence +const getRepoName = (): string => { + if (fs.existsSync(repoNameFile)) { + const repoName = fs.readFileSync(repoNameFile, 'utf8'); + console.log(`Loaded repo name from file: ${repoName}`); + return repoName; + } + const repoName = `${repoNamePrefix}-${randomName()}`; + fs.writeFileSync(repoNameFile, repoName); + console.log(`Generated and saved repo name: ${repoName}`); + return repoName; +}; + +const url = randomUrl(); + +test.describe('Combined user tests', () => { + test('Login as user 1 (admin)', async ({ page }) => { + await test.step('Navigate to the repository page', async () => { + // Clean up the repo name file + if (fs.existsSync(repoNameFile)) { + fs.unlinkSync(repoNameFile); + } + await deleteAllRepos(page, `&search=${repoNamePrefix}`); + await navigateToRepositories(page); + await closePopupsIfExist(page); + }); + + await test.step('Create a repository', async () => { + await page.getByRole('button', { name: 'Add repositories' }).first().click(); + await expect(page.getByRole('dialog', { name: 'Add custom repositories' })).toBeVisible(); + + const repoName = getRepoName(); + await page.getByLabel('Name').fill(repoName); + await page.getByLabel('Introspect only').click(); + await page.getByLabel('URL').fill(url); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + }); + + await test.step('Read the repo', async () => { + const repoName = getRepoName(); + const row = await getRowByNameOrUrl(page, repoName); + await expect(row.getByText('Valid')).toBeVisible(); + await row.getByLabel('Kebab toggle').click(); + await row.getByRole('menuitem', { name: 'Edit' }).click(); + await expect(page.getByRole('dialog', { name: 'Edit custom repository' })).toBeVisible(); + await expect(page.getByPlaceholder('Enter name', { exact: true })).toHaveValue(repoName); + await expect(page.getByPlaceholder('https://', { exact: true })).toHaveValue(url); + }); + + await test.step('Update the repository', async () => { + const repoName = getRepoName(); + await page.getByPlaceholder('Enter name', { exact: true }).fill(`${repoName}-Edited`); + await page.getByRole('button', { name: 'Save changes', exact: true }).click(); + }); + }); + + test('Login as user 2 (read-only)', { tag: '@read-only' }, async ({ page }) => { + await test.step('Navigate to the repository page', async () => { + await navigateToRepositories(page); + await closePopupsIfExist(page); + }); + + await test.step('Read the repo', async () => { + const repoName = getRepoName(); + const row = await getRowByNameOrUrl(page, `${repoName}-Edited`); + await expect(row.getByText('Valid')).toBeVisible({ timeout: 60000 }); + await row.getByLabel('Kebab toggle').click(); + await expect(row.getByRole('menuitem', { name: 'Edit' })).not.toBeVisible(); + }); + }); +}); diff --git a/_playwright-tests/UI/helpers/navHelpers.ts b/_playwright-tests/UI/helpers/navHelpers.ts index 66f9c4569..3c64e6104 100644 --- a/_playwright-tests/UI/helpers/navHelpers.ts +++ b/_playwright-tests/UI/helpers/navHelpers.ts @@ -2,7 +2,7 @@ import { type Page } from '@playwright/test'; import { retry } from './helpers'; const navigateToRepositoriesFunc = async (page: Page) => { - await page.goto('/insights/content/repositories', { timeout: 10001 }); + await page.goto('/insights/content/repositories', { timeout: 20000 }); const zeroState = page.getByText('Start using Content management now'); @@ -31,7 +31,7 @@ export const navigateToRepositories = async (page: Page) => { await page.route('https://smetrics.redhat.com/**', (route) => route.abort()); const repositoriesNavLink = page - .getByRole('navigation') + .getByRole('navigation', { name: 'Breadcrumb' }) .getByRole('link', { name: 'Repositories' }); await repositoriesNavLink.waitFor({ state: 'visible', timeout: 1500 }); await repositoriesNavLink.click(); diff --git a/_playwright-tests/auth.setup.ts b/_playwright-tests/auth.setup.ts index a35a103bb..08d37e122 100644 --- a/_playwright-tests/auth.setup.ts +++ b/_playwright-tests/auth.setup.ts @@ -1,23 +1,37 @@ -import { expect, test as setup } from '@playwright/test'; +import { expect, test as setup, type Page } from '@playwright/test'; import { throwIfMissingEnvVariables, - logInWithUser1, - storeStorageStateAndToken, + logInWithUsernameAndPassword, + logout, + switchToUser, } from './helpers/loginHelpers'; +import { describe } from 'node:test'; import { closePopupsIfExist } from './UI/helpers/helpers'; -setup.describe('Setup', async () => { - setup.describe.configure({ retries: 3 }); - - setup('Ensure needed ENV variables exist', async () => { +describe('Setup', async () => { + setup('Ensure needed ENV variables exist', async ({}) => { expect(() => throwIfMissingEnvVariables()).not.toThrow(); }); - setup('Authenticate', async ({ page }) => { - setup.setTimeout(60_000); - + setup('Authenticate all the users', async ({ page }) => { await closePopupsIfExist(page); - await logInWithUser1(page); - await storeStorageStateAndToken(page); + + await logInWithUsernameAndPassword( + page, + process.env.READONLY_USERNAME, + process.env.READONLY_PASSWORD, + ); + + await logout(page); + + await logInWithUsernameAndPassword( + page, + process.env.ADMIN_USERNAME, + process.env.ADMIN_PASSWORD, + ); + + await switchToUser(page, process.env.ADMIN_USERNAME!); + + // We do this as we run admin tests first. }); }); diff --git a/_playwright-tests/helpers/loginHelpers.ts b/_playwright-tests/helpers/loginHelpers.ts new file mode 100644 index 000000000..ad27eca7b --- /dev/null +++ b/_playwright-tests/helpers/loginHelpers.ts @@ -0,0 +1,152 @@ +import { expect, type Page } from '@playwright/test'; +import path from 'path'; +import fs from 'fs'; + +export const logout = async (page: Page) => { + const button = await page.locator('.pf-v6-c-menu-toggle.data-hj-suppress.sentry-mask').first(); + + await button.click(); + + await expect(async () => page.getByRole('menuitem', { name: 'Log out' }).isVisible()).toPass(); + + await page.getByRole('menuitem', { name: 'Log out' }).click(); + + await expect(async () => { + expect(page.url()).not.toBe('/insights/content/repositories'); + }).toPass(); +}; + +// Inline reading and parsing of the JSON file +const queryJsonFile = (filePath: string) => { + try { + const data = fs.readFileSync(filePath, 'utf-8'); // Read the file synchronously + const jsonData = JSON.parse(data); // Parse the JSON data + return jsonData; // Return the parsed JSON data + } catch (error) { + console.error('Error reading or parsing the JSON file:', error); + return null; + } +}; + +export const switchToUser = async (page: Page, userName: string) => { + const storagePath = path.join(__dirname, `../../.auth/${userName}.json`); + const storedData = queryJsonFile(storagePath); + + const jwtCookie = storedData.cookies.find((cookie: { name: string }) => cookie.name === 'cs_jwt'); + if (!jwtCookie || !jwtCookie.value) { + throw new Error( + `No valid cs_jwt cookie found in storage state for user ${userName} at ${storagePath}`, + ); + } + + // This is the main thing that this function does, sets the jwt for the API! + process.env.TOKEN = `Bearer ${jwtCookie.value}`; + await page.waitForTimeout(100); +}; + +export const storeUserAuth = async (page: Page, userName: string) => { + const storagePath = path.join(__dirname, `../../.auth/${userName}.json`); + // this stores the data in the json file at .auth/xxxx.json + await page.context().storageState({ + path: storagePath, + }); +}; + +export const logInWithUsernameAndPassword = async ( + page: Page, + username?: string, + password?: string, +) => { + if (!username || !password) { + throw new Error('Username or password not found'); + } + + await page.goto('/insights/content/repositories'); + + await expect(async () => { + expect(page.url()).not.toBe(process.env.BASE_URL + '/insights/content/repositories'); + }).toPass(); + + await expect(async () => + expect(page.getByText('Log in to your Red Hat account')).toBeVisible(), + ).toPass(); + const login = page.getByRole('textbox'); + await login.fill(username); + await login.press('Enter'); + const passwordField = page.getByRole('textbox', { name: 'Password' }); + await passwordField.fill(password); + await passwordField.press('Enter'); + + await expect( + page + .getByText('View all repositories within your organization.') + .or(page.getByText('Add repositories now', { exact: true })), + ).toBeVisible(); + + await storeUserAuth(page, username); +}; + +export const closePopupsIfExist = async (page: Page) => { + const locatorsToCheck = [ + page.locator('[class*="c-modal-box__close"] > button'), + page.locator('[class*="c-alert"][class*="notification-item"] button'), // This closes all toast pop-ups + page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up + page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pup-up + page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.) + ]; + + for (const locator of locatorsToCheck) { + await page.addLocatorHandler(locator, async () => { + await locator.click(); + }); + } +}; + +export const throwIfMissingEnvVariables = () => { + const ManditoryEnvVariables = [ + 'ADMIN_USERNAME', + 'ADMIN_PASSWORD', + 'BASE_URL', + 'ORG_ID_1', + 'ACTIVATION_KEY_1', + ]; + + if (!process.env.PROD) ManditoryEnvVariables.push('PROXY'); + + const missing: string[] = []; + ManditoryEnvVariables.forEach((envVar) => { + if (!process.env[envVar]) { + missing.push(envVar); + } + }); + + if (missing.length > 0) { + throw new Error('Missing env variables:' + missing.join(',')); + } + + if (process.env.PROXY && process.env.BASE_URL?.includes('stage.foo.redhat')) { + throw new Error( + "If testing against a local machine you need to unset '' your proxy in the .env file!", + ); + } +}; + +export const ensureNotInPreview = async (page: Page) => { + const toggle = page.locator('.pf-v6-c-switch__toggle'); + if ((await toggle.isVisible()) && (await toggle.isChecked())) { + await toggle.click(); + } +}; + +export const ensureInPreview = async (page: Page) => { + const toggle = page.locator('.pf-v6-c-switch__toggle'); + await expect(toggle).toBeVisible(); + if (!(await toggle.isChecked())) { + await toggle.click(); + } + const turnOnButton = page.getByRole('button', { name: 'Turn on' }); + if (await turnOnButton.isVisible()) { + await turnOnButton.click(); + } + await expect(toggle).toBeChecked(); +}; diff --git a/_playwright-tests/helpers/loginHelpers.tsx b/_playwright-tests/helpers/loginHelpers.tsx deleted file mode 100644 index a58059591..000000000 --- a/_playwright-tests/helpers/loginHelpers.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { expect, type Page } from '@playwright/test'; -import path from 'path'; - -// This file can only contain functions that are referenced by authentication. - -export const logout = async (page: Page) => { - const button = await page.locator( - 'div.pf-v5-c-toolbar__item.pf-m-hidden.pf-m-visible-on-lg.pf-v5-u-mr-0 > button', - ); - - await button.click(); - - await expect(async () => page.getByRole('menuitem', { name: 'Log out' }).isVisible()).toPass(); - - await page.getByRole('menuitem', { name: 'Log out' }).click(); - - await expect(async () => { - expect(page.url()).not.toBe('/insights/content/repositories'); - }).toPass(); - await expect(async () => - expect(page.getByText('Log in to your Red Hat account')).toBeVisible(), - ).toPass(); -}; - -export const logInWithUsernameAndPassword = async ( - page: Page, - username?: string, - password?: string, -) => { - if (!username || !password) { - throw new Error('Username or password not found'); - } - - await page.goto('/insights/content/repositories'); - - await expect(async () => - expect(page.getByText('Log in to your Red Hat account')).toBeVisible(), - ).toPass(); - - const login = page.getByRole('textbox'); - await login.fill(username); - await login.press('Enter'); - const passwordField = page.getByRole('textbox', { name: 'Password' }); - await passwordField.fill(password); - await passwordField.press('Enter'); - - await expect(async () => { - expect(page.url()).toBe(`${process.env.BASE_URL}/insights/content/repositories`); - }).toPass({ - intervals: [1_000], - timeout: 30_000, - }); -}; - -export const logInWithUser1 = async (page: Page) => - await logInWithUsernameAndPassword(page, process.env.USER1USERNAME, process.env.USER1PASSWORD); - -export const storeStorageStateAndToken = async (page: Page) => { - const { cookies } = await page - .context() - .storageState({ path: path.join(__dirname, '../../.auth/user.json') }); - process.env.TOKEN = `Bearer ${cookies.find((cookie) => cookie.name === 'cs_jwt')?.value}`; - await page.waitForTimeout(100); -}; - -export const throwIfMissingEnvVariables = () => { - const ManditoryEnvVariables = ['USER1USERNAME', 'USER1PASSWORD', 'BASE_URL']; - - const missing: string[] = []; - ManditoryEnvVariables.forEach((envVar) => { - if (!process.env[envVar]) { - missing.push(envVar); - } - }); - - if (missing.length > 0) { - throw new Error('Missing env variables:' + missing.join(',')); - } -}; diff --git a/playwright.config.ts b/playwright.config.ts index e00f782fb..009a2e56e 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -53,12 +53,27 @@ export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/ }, { - name: 'chromium', + name: 'AdminTests', // 'Run admin user tests', + grepInvert: [/read-only/], // !!process.env.PROD ? [/preview-only/, /switch-to-preview/], ] : [/switch-to-preview/], use: { ...devices['Desktop Chrome'], - storageState: '.auth/user.json', + storageState: `./.auth/${process.env.ADMIN_USERNAME}.json`, }, dependencies: ['setup'], }, + { + name: "SwitchToUser2", + testMatch: /.switchToUser2\.setup\.ts/, + dependencies: ["setup"], + }, + { + name: "ReadOnlyTests", // 'Run read-only user tests', + grep: [/read-only/], + use: { + ...devices["Desktop Chrome"], + storageState: `.auth/${process.env.READONLY_USERNAME}.json`, + }, + dependencies: ["SwitchToUser2"], + }, ], }); diff --git a/switchToUser2.setup.ts b/switchToUser2.setup.ts new file mode 100644 index 000000000..1b069eed4 --- /dev/null +++ b/switchToUser2.setup.ts @@ -0,0 +1,7 @@ +import { expect, test as setup, type Page } from "@playwright/test"; +import { throwIfMissingEnvVariables, switchToUser } from './helpers/loginHelpers'; +import { describe } from "node:test"; + +setup("Switch to user 2", async ({ page }) => { + await switchToUser(page, process.env.READONLY_USERNAME!); +});