Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ui-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
E2E_GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_PERSONAL_ACCESS_TOKEN }}
E2E_GITHUB_ORGANIZATION: ${{ secrets.E2E_GITHUB_ORGANIZATION }}
E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN }}
E2E_ORGANIZATION_ID: ${{ secrets.E2E_ORGANIZATION_ID }}
E2E_NEW_USER_PASSWORD: ${{ secrets.E2E_NEW_USER_PASSWORD }}

steps:
Expand Down
8 changes: 4 additions & 4 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
"format:check": "./node_modules/.bin/prettier --check ./app",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
"prepare": "husky",
"test:e2e": "playwright test --project=chromium --project=sign-up --project=providers",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --project=providers --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --project=providers --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --project=providers --headed",
"test:e2e": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --headed",
"test:e2e:report": "playwright show-report",
"test:e2e:install": "playwright install"
},
Expand Down
6 changes: 6 additions & 0 deletions ui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ export default defineConfig({
testMatch: "providers.spec.ts",
dependencies: ["admin.auth.setup"],
},
// This project runs the invitations test suite
{
name: "invitations",
testMatch: "invitations.spec.ts",
dependencies: ["admin.auth.setup"],
},
],

webServer: {
Expand Down
113 changes: 113 additions & 0 deletions ui/tests/invitations/invitations-page.ts
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();

if (!text) {
throw new Error("Share url not found");
}
return text;
}
}
66 changes: 66 additions & 0 deletions ui/tests/invitations/invitations.md
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
105 changes: 105 additions & 0 deletions ui/tests/invitations/invitations.spec.ts
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();
},
);
});
32 changes: 32 additions & 0 deletions ui/tests/profile/profile-page.ts
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`);
}
}
}
2 changes: 1 addition & 1 deletion ui/tests/providers/providers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
### E2E Tests: AWS Provider Management

**Suite ID:** `PROVIDER-E2E`
**Feature:** AWS Provider Management - Add and configure AWS cloud providers with different authentication methods
**Feature:** AWS Provider Management.

---

Expand Down
Loading