diff --git a/.github/workflows/stageTestAction.yml b/.github/workflows/stageTestAction.yml index b38c1be..8e176fa 100644 --- a/.github/workflows/stageTestAction.yml +++ b/.github/workflows/stageTestAction.yml @@ -41,8 +41,8 @@ jobs: - name: Create .env file run: | - echo "USER1USERNAME=$USER1USERNAME" >> .env - echo "USER1PASSWORD=$USER1PASSWORD" >> .env + echo "ADMIN_USERNAME=$ADMIN_USERNAME" >> .env + echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> .env echo "BASE_URL=$BASE_URL" >> .env echo "PROXY=$PROXY" >> .env echo "TOKEN=apple" >> .env diff --git a/.gitignore b/.gitignore index 694302a..f896950 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ node_modules/ .DS_Store .env .auth -bin \ No newline at end of file +bin +repoName.txt diff --git a/example.env b/example.env index dea756b..af84c18 100644 --- a/example.env +++ b/example.env @@ -1,8 +1,11 @@ #skus for qe user contentPlaywrightUserAdmin: # MCT4022,MCT3718,MCT3695,ES0113909 -USER1USERNAME="contentPlaywrightUserAdmin" # Required -USER1PASSWORD="" # Required (Ask Andrew if needed) +ADMIN_USERNAME="contentPlaywrightUserAdmin" +ADMIN_PASSWORD=SOMETHINGSECRET + +READONLY_USERNAME="contentPlaywrightReader" +READONLY_PASSWORD=SOMETHINGSECRET ORG_ID_1="1234" #org id to register for registration tests ACTIVATION_KEY_1="MyKey" #activation Key used for testing diff --git a/playwright.config.ts b/playwright.config.ts index bda2f8a..4681256 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,80 +1,80 @@ -import { defineConfig, devices } from '@playwright/test'; -import 'dotenv/config'; +import { defineConfig, devices } from "@playwright/test"; +import "dotenv/config"; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', - fullyParallel: false, - forbidOnly: false, - retries: process.env.CI ? 1 : 0, - workers: 1, - reporter: process.env.CI - ? [ - [ - 'playwright-ctrf-json-reporter', - { useDetails: true, outputDir: 'playwright-ctrf', outputFile: 'playwright-ctrf.json' }, - ], - ['html', { outputFolder: 'playwright-report' }], - ['@currents/playwright'], - ] - : 'list', - timeout: process.env.CI ? 60000 : 30000, - expect: { timeout: process.env.CI ? 60000 : 20000 }, - use: { - testIdAttribute: 'data-ouia-component-id', - launchOptions: { - args: ['--use-fake-device-for-media-stream'], - }, - ...(process.env.TOKEN - ? { - extraHTTPHeaders: { - Authorization: process.env.TOKEN, - }, - } - : {}), - baseURL: process.env.BASE_URL, - trace: 'on', - screenshot: 'on', - video: 'on', - ignoreHTTPSErrors: true, - ...process.env.PROXY ? { - proxy: { - server: process.env.PROXY, - } - } : {} + testDir: "./tests", + fullyParallel: false, + forbidOnly: false, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI + ? [ + [ + "playwright-ctrf-json-reporter", + { + useDetails: true, + outputDir: "playwright-ctrf", + outputFile: "playwright-ctrf.json", + }, + ], + ["html", { outputFolder: "playwright-report" }], + ["@currents/playwright"], + ] + : "list", + timeout: process.env.CI ? 60000 : 30000, + expect: { timeout: process.env.CI ? 60000 : 20000 }, + use: { + testIdAttribute: "data-ouia-component-id", + launchOptions: { + args: ["--use-fake-device-for-media-stream"], }, - projects: [ - { name: 'setup', testMatch: /.*\.setup\.ts/ }, - { - name: 'chromium', - grepInvert: !!process.env.PROD ? [/preview-only/, /switch-to-preview/, /local-only/] : [/switch-to-preview/, /local-only/], - use: { - ...devices['Desktop Chrome'], - storageState: `.auth/${process.env.USER1USERNAME}.json`, - }, - dependencies: ['setup'], - }, - ...!!process.env.PROD ? - [{ - name: 'Switch to preview', - grep: [/switch-to-preview/], - use: { - ...devices['Desktop Chrome'], - storageState: `.auth/${process.env.USER1USERNAME}.json`, - - }, - dependencies: ['setup'],//'chromium', - }, - { - name: 'Run preview only', - grep: [/preview-only/], - use: { - ...devices['Desktop Chrome'], - storageState: `.auth/${process.env.USER1USERNAME}.json`, - }, - dependencies: ['Switch to preview'], - }] : [], - ], + ...(process.env.TOKEN + ? { + extraHTTPHeaders: { + Authorization: process.env.TOKEN, + }, + } + : {}), + baseURL: process.env.BASE_URL, + trace: "on", + screenshot: "on", + video: "on", + ignoreHTTPSErrors: true, + ...(process.env.PROXY + ? { + proxy: { + server: process.env.PROXY, + }, + } + : {}), + }, + projects: [ + { name: "setup", testMatch: /auth\.setup\.ts/ }, + { + 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/${process.env.ADMIN_USERNAME}.json`, // This is setting the cookies + }, + 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/readme.md b/readme.md index 117d6fa..023c867 100644 --- a/readme.md +++ b/readme.md @@ -19,20 +19,21 @@ yarn get-tests ## Podman As your user, run podman to serve the api: + ``` podman system service -t 0 unix:///tmp/podman.sock ``` Uncomment the DOCKER_SOCKET option in the .env file: + ``` DOCKER_SOCKET="/tmp/podman.sock" ``` ## Docker -* ensure the docker service is running -* ensure your user is part of the 'docker' user group - +- ensure the docker service is running +- ensure your user is part of the 'docker' user group # Option 1 Run local: @@ -40,8 +41,8 @@ For local testing, make sure your front-end/backend servers are running and acce - Ensure you do NOT specify a proxy in your .env file (put an empty value: "") - Make sure your .env's BASE_URL is pointed to the local front-end server "https://stage.foo.redhat.com:1337" -- USER1USERNAME="" -- USER1PASSWORD="" +- ADMIN_USERNAME="" +- ADMIN_PASSWORD="" Note: For Ethel, your user will require the following skus: MCT4022,MCT3718,MCT3695,ES0113909 @@ -51,8 +52,8 @@ For local stage testing, make sure the following: - PROXY must be set correctly in your .env file. - Make sure your .env's BASE_URL is pointed to the targeted env, if targeting PROD, the proxy is not needed. -- USER1USERNAME="" -- USER1PASSWORD="" +- ADMIN_USERNAME="" +- ADMIN_PASSWORD="" Note: For Ethel, your user will require the following skus: MCT4022,MCT3718,MCT3695,ES0113909 diff --git a/tests/Integration/switchToPreview.spec.ts b/tests/Integration/switchToPreview.spec.ts deleted file mode 100644 index d5d9994..0000000 --- a/tests/Integration/switchToPreview.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { navigateToRepositories } from "../UI/helpers/navHelpers"; -import { ensureInPreview } from "../helpers/loginHelpers"; - -test.describe("Switch to preview", { tag: "@switch-to-preview" }, async () => { - test("Click preview button", async ({ page }) => { - await navigateToRepositories(page); - await ensureInPreview(page) - }); -}); diff --git a/tests/Integration/twouserRBACTest.spec.ts b/tests/Integration/twouserRBACTest.spec.ts new file mode 100644 index 0000000..e9cb53d --- /dev/null +++ b/tests/Integration/twouserRBACTest.spec.ts @@ -0,0 +1,105 @@ +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"; +import { switchToUser } from "../helpers/loginHelpers"; + +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); + }; + console.log("Cleaned up repoName.txt"); + console.log("\n Try to delete old repos\n"); + 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/tests/auth.setup.ts b/tests/auth.setup.ts index 251095f..32055eb 100644 --- a/tests/auth.setup.ts +++ b/tests/auth.setup.ts @@ -4,32 +4,40 @@ import { closePopupsIfExist, switchToUser, logInWithUsernameAndPassword, - ensureNotInPreview, + logout, } from "./helpers/loginHelpers"; import { describe } from "node:test"; +import { existsSync, mkdirSync } from 'fs'; +const authDir = '.auth'; +if (!existsSync(authDir)) { + mkdirSync(authDir); +} + describe("Setup", async () => { setup("Ensure needed ENV variables exist", async ({}) => { expect(() => throwIfMissingEnvVariables()).not.toThrow(); }); - setup("Authenticate user 1", async ({ page }) => { + setup("Authenticate all the users", async ({ page }) => { await closePopupsIfExist(page); + await logInWithUsernameAndPassword( page, - process.env.USER1USERNAME, - process.env.USER1PASSWORD + process.env.READONLY_USERNAME, + process.env.READONLY_PASSWORD ); - // Example of how to add another user - // await logout(page) - // await logInWithUsernameAndPassword( - // page, - // process.env.USER2USERNAME, - // process.env.USER2PASSWORD - // ); - // Example of how to switch to said user - await switchToUser(page, process.env.USER1USERNAME!); - await ensureNotInPreview(page); - // Other users for other tests can be added below after logging out + + 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/tests/helpers/loginHelpers.ts b/tests/helpers/loginHelpers.ts index 403401e..bf0239f 100644 --- a/tests/helpers/loginHelpers.ts +++ b/tests/helpers/loginHelpers.ts @@ -1,5 +1,6 @@ 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 @@ -19,6 +20,44 @@ export const logout = async (page: Page) => { }).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, @@ -46,27 +85,19 @@ export const logInWithUsernameAndPassword = async ( await passwordField.fill(password); await passwordField.press("Enter"); - await expect(async () => { - expect(page.url()).toBe( - `${process.env.BASE_URL}/insights/content/repositories` - ); - }).toPass(); -}; -export const switchToUser = async (page: Page, userName: string) => { - const { cookies } = await page.context().storageState({ - path: path.join(__dirname, `../../.auth/${userName}.json`), - }); - process.env.TOKEN = `Bearer ${ - cookies.find((cookie) => cookie.name === "cs_jwt")?.value - }`; - await page.waitForTimeout(100); + 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(".pf-v6-c-modal-box__close > button"), - page.locator(".pf-v5-c-alert.notification-item button"), // This closes all toast pop-ups + 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.) @@ -81,8 +112,8 @@ export const closePopupsIfExist = async (page: Page) => { export const throwIfMissingEnvVariables = () => { const ManditoryEnvVariables = [ - "USER1USERNAME", - "USER1PASSWORD", + "ADMIN_USERNAME", + "ADMIN_PASSWORD", "BASE_URL", "ORG_ID_1", "ACTIVATION_KEY_1", diff --git a/tests/switchToUser2.setup.ts b/tests/switchToUser2.setup.ts new file mode 100644 index 0000000..52a6047 --- /dev/null +++ b/tests/switchToUser2.setup.ts @@ -0,0 +1,10 @@ +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!); +});