-
Notifications
You must be signed in to change notification settings - Fork 1.8k
test(ui): Invite User E2E tests #9045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
StylusFrost
wants to merge
3
commits into
PROWLER-197-out-of-scope-github-add-an-connect-the-provider
Choose a base branch
from
PROWLER-188-invite-new-user
base: PROWLER-197-out-of-scope-github-add-an-connect-the-provider
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+361
−9
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { Page, Locator, expect } from "@playwright/test"; | ||
| import { BasePage } from "../base-page"; | ||
|
|
||
|
|
||
| export class InvitationsPage extends BasePage { | ||
|
|
||
| // Page heading | ||
| readonly pageHeadingSendInvitation: Locator; | ||
| readonly pageHeadingInvitations: Locator; | ||
|
|
||
| // UI elements | ||
| readonly sendInviteButton: Locator; | ||
| readonly emailInput: Locator; | ||
| readonly roleSelect: Locator; | ||
|
|
||
| // Invitation details | ||
| readonly invitationDetails: Locator; | ||
| readonly shareUrl: Locator; | ||
|
|
||
|
|
||
| constructor(page: Page) { | ||
| super(page); | ||
|
|
||
| // Page heading | ||
| this.pageHeadingInvitations = page.getByRole("heading", { name: "Invitations" }); | ||
| this.pageHeadingSendInvitation = page.getByRole("heading", { name: "Send Invitation" }); | ||
|
|
||
| // Button to invite a new user | ||
| this.sendInviteButton = page.getByRole("button", { name: "Send Invitation", exact: true }); | ||
|
|
||
| // Form inputs | ||
| this.emailInput = page.getByRole("textbox", { name: "Email" }); | ||
|
|
||
| // Form select | ||
| this.roleSelect = page.getByRole("button", { name: /Role|Select a role/i }); | ||
|
|
||
| // Form details | ||
| this.invitationDetails = page.getByRole('heading', { name: 'Invitation details' }); | ||
|
|
||
| // Multiple strategies to find the share URL | ||
| this.shareUrl = page.locator('a[href*="/sign-up?invitation_token="], [data-testid="share-url"], .share-url, code, pre').first(); | ||
| } | ||
|
|
||
| async goto(): Promise<void> { | ||
| // Navigate to the invitations page | ||
|
|
||
| await super.goto("/invitations"); | ||
| } | ||
|
|
||
| async clickSendInviteButton(): Promise<void> { | ||
| // Click the send invitation button | ||
|
|
||
| await this.sendInviteButton.click(); | ||
| await this.waitForPageLoad(); | ||
| } | ||
|
|
||
| async verifyPageLoaded(): Promise<void> { | ||
| // Verify the invitations page is loaded | ||
|
|
||
| await expect(this.pageHeadingInvitations).toBeVisible(); | ||
| await this.waitForPageLoad(); | ||
| } | ||
|
|
||
| async verifyInvitePageLoaded(): Promise<void> { | ||
| // Verify the invite page is loaded | ||
|
|
||
| await expect(this.emailInput).toBeVisible(); | ||
| await expect(this.sendInviteButton).toBeVisible(); | ||
| await this.waitForPageLoad(); | ||
| } | ||
|
|
||
| async fillEmail(email: string): Promise<void> { | ||
| // Fill the email input | ||
| await this.emailInput.fill(email); | ||
| } | ||
|
|
||
| async selectRole(role: string): Promise<void> { | ||
| // Select the role option | ||
|
|
||
| // Open the role dropdown | ||
| await this.roleSelect.click(); | ||
|
|
||
| // Prefer ARIA role option inside listbox | ||
| const option = this.page.getByRole("option", { name: new RegExp(`^${role}$`, "i") }); | ||
|
|
||
| if (await option.count()) { | ||
| await option.first().click(); | ||
| } else { | ||
| throw new Error(`Role option ${role} not found`); | ||
| } | ||
| // Ensure the combobox now shows the chosen value | ||
| await expect(this.roleSelect).toContainText(new RegExp(role, "i")); | ||
| } | ||
|
|
||
| async verifyInviteDataPageLoaded(): Promise<void> { | ||
| // Verify the invite data page is loaded | ||
|
|
||
| await expect(this.invitationDetails).toBeVisible(); | ||
| await this.waitForPageLoad(); | ||
| } | ||
|
|
||
| async getShareUrl(): Promise<string> { | ||
| // Get the share url | ||
|
|
||
| // Get the share url text content | ||
| const text = await this.shareUrl.textContent(); | ||
StylusFrost marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (!text) { | ||
| throw new Error("Share url not found"); | ||
| } | ||
| return text; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| ### E2E Tests: Invitations Management | ||
|
|
||
| **Suite ID:** `INVITATION-E2E` | ||
| **Feature:** User Invitations. | ||
|
|
||
| --- | ||
|
|
||
| ## Test Case: `INVITATION-E2E-001` - Invite New User and Complete Sign-Up | ||
|
|
||
| **Priority:** `critical` | ||
|
|
||
| **Tags:** | ||
|
|
||
| - type → @e2e | ||
| - feature → @invitations | ||
| - id → @INVITATION-E2E-001 | ||
|
|
||
| **Description/Objective:** Validates the full flow to invite a new user from the admin session, consume the invitation link, sign up as the invited user, authenticate, and verify the user is associated to the expected organization. | ||
|
|
||
| **Preconditions:** | ||
|
|
||
| - Admin authentication state available: `playwright/.auth/admin_user.json` (admin.auth.setup) | ||
| - Environment variables configured: | ||
| - `E2E_NEW_USER_PASSWORD` (password for the invited user) | ||
| - `E2E_ORGANIZATION_ID` (expected organization for membership verification) | ||
| - Application running with accessible UI/API endpoints | ||
|
|
||
| ### Flow Steps: | ||
|
|
||
| 1. Navigate to invitations page | ||
| 2. Click "Send Invitation" button | ||
| 3. Fill unique email address for the invite | ||
| 4. Select role `e2e_admin` | ||
| 5. Click "Send Invitation" to generate invitation | ||
| 6. Read the generated share URL from the invitation details | ||
| 7. Open a new browser context (no admin cookies) and navigate to the share URL | ||
| 8. Complete sign-up with provided password and accept terms | ||
| 9. Verify sign-up success (no errors) and redirect to login page | ||
| 10. Log in with the newly created credentials in the new context | ||
| 11. Verify successful login | ||
| 12. Navigate to user profile and verify `organizationId` matches `E2E_ORGANIZATION_ID` | ||
|
|
||
| ### Expected Result: | ||
|
|
||
| - Invitation is created and a valid share URL is provided | ||
| - Invited user can sign up successfully using the invitation link | ||
| - User is redirected to the login page after sign-up (OSS flow) | ||
| - Login succeeds with the new credentials | ||
| - User profile shows membership in the expected organization | ||
|
|
||
| ### Key verification points: | ||
|
|
||
| - Invitations page loads and displays the heading | ||
| - Send Invitation form is visible (email + role select) | ||
| - Invitation details page shows share URL | ||
| - Sign-up page loads from invitation link and submits without errors | ||
| - Post sign-up, redirect to login is performed | ||
| - Login with the new account succeeds | ||
| - Profile page shows the expected organization id | ||
|
|
||
| ### Notes: | ||
|
|
||
| - Test uses a fresh browser context for the invitee to avoid admin session leakage | ||
| - Email should be unique per run (the test uses a random suffix) | ||
| - Ensure `E2E_NEW_USER_PASSWORD` and `E2E_ORGANIZATION_ID` are set before execution | ||
| - The role `e2e_admin` must be available in the environment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { test } from "@playwright/test"; | ||
| import { InvitationsPage } from "./invitations-page"; | ||
| import { makeSuffix } from "../helpers"; | ||
| import { SignUpPage } from "../sign-up/sign-up-page"; | ||
| import { SignInPage } from "../sign-in/sign-in-page"; | ||
| import { UserProfilePage } from "../profile/profile-page"; | ||
|
|
||
| test.describe("New user invitation", () => { | ||
| // Invitations page object | ||
| let invitationsPage: InvitationsPage; | ||
|
|
||
| // Setup before each test | ||
| test.beforeEach(async ({ page }) => { | ||
| invitationsPage = new InvitationsPage(page); | ||
| }); | ||
|
|
||
| // Use admin authentication for invitations management | ||
| test.use({ storageState: "playwright/.auth/admin_user.json" }); | ||
|
|
||
| test( | ||
| "should invite a new user", | ||
| { | ||
| tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-001"], | ||
| }, | ||
| async ({ page, browser }) => { | ||
|
|
||
| // Test data from environment variables | ||
| const password = process.env.E2E_NEW_USER_PASSWORD; | ||
| const organizationId = process.env.E2E_ORGANIZATION_ID; | ||
|
|
||
| // Validate required environment variables | ||
| if (!password || !organizationId) { | ||
| throw new Error( | ||
| "E2E_NEW_USER_PASSWORD or E2E_ORGANIZATION_ID environment variable is not set", | ||
| ); | ||
| } | ||
|
|
||
| // Generate unique test data | ||
| const suffix = makeSuffix(10); | ||
| const uniqueEmail = `e2e+${suffix}@prowler.com`; | ||
|
|
||
| // Navigate to providers page | ||
| await invitationsPage.goto(); | ||
| await invitationsPage.verifyPageLoaded(); | ||
|
|
||
| // Press the invite button | ||
| await invitationsPage.clickSendInviteButton(); | ||
| await invitationsPage.verifyInvitePageLoaded(); | ||
|
|
||
| // Fill the email | ||
| await invitationsPage.fillEmail(uniqueEmail); | ||
|
|
||
| // Select the role option | ||
| await invitationsPage.selectRole("e2e_admin"); | ||
|
|
||
| // Press the send invitation button | ||
| await invitationsPage.clickSendInviteButton(); | ||
| await invitationsPage.verifyInviteDataPageLoaded(); | ||
|
|
||
| // Get the share url | ||
| const shareUrl = await invitationsPage.getShareUrl(); | ||
|
|
||
| // Navigate to the share url with a new context to avoid cookies from the admin context | ||
| const inviteContext = await browser.newContext({ storageState: { cookies: [], origins: [] } }); | ||
| const signUpPage = new SignUpPage(await inviteContext.newPage()); | ||
|
|
||
| // Navigate to the share url | ||
| await signUpPage.gotoInvite(shareUrl); | ||
|
|
||
| // Fill and submit the sign-up form | ||
| await signUpPage.signup({ | ||
| name: `E2E User ${suffix}`, | ||
| email: uniqueEmail, | ||
| password: password, | ||
| confirmPassword: password, | ||
| acceptTerms: true, | ||
| }); | ||
|
|
||
| // Verify no errors occurred during sign-up | ||
| await signUpPage.verifyNoErrors(); | ||
|
|
||
| // Verify redirect to login page (OSS environment) | ||
| await signUpPage.verifyRedirectToLogin(); | ||
|
|
||
| // Verify the newly created user can log in successfully with the new context | ||
| const signInPage = new SignInPage(await inviteContext.newPage()); | ||
| await signInPage.goto(); | ||
| await signInPage.login({ | ||
| email: uniqueEmail, | ||
| password: password, | ||
| }); | ||
| await signInPage.verifySuccessfulLogin(); | ||
|
|
||
| // Navigate to the user profile page | ||
| const userProfilePage = new UserProfilePage(await inviteContext.newPage()); | ||
| await userProfilePage.goto(); | ||
|
|
||
| // Verify if user is added to the organization | ||
| await userProfilePage.verifyOrganizationId(organizationId); | ||
|
|
||
| // Close the invite context | ||
| await inviteContext.close(); | ||
| }, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { Page, Locator, expect } from "@playwright/test"; | ||
| import { BasePage } from "../base-page"; | ||
|
|
||
|
|
||
| export class UserProfilePage extends BasePage { | ||
|
|
||
| // Page heading | ||
| readonly pageHeadingUserProfile: Locator; | ||
|
|
||
| constructor(page: Page) { | ||
| super(page); | ||
|
|
||
| // Page heading | ||
| this.pageHeadingUserProfile = page.getByRole("heading", { name: "User Profile" }); | ||
|
|
||
| } | ||
|
|
||
| async goto(): Promise<void> { | ||
| // Navigate to the user profile page | ||
|
|
||
| await super.goto("/profile"); | ||
| } | ||
|
|
||
| async verifyOrganizationId(organizationId: string): Promise<void> { | ||
| // Verify the organization ID is visible | ||
|
|
||
| await expect(this.page.getByText(organizationId)).toBeVisible(); | ||
| if (await this.page.getByText(organizationId).count() === 0) { | ||
| throw new Error(`Organization ID ${organizationId} not found`); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.