test(ui): add E2E tests for invitation accept smart router (#10814)

Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
This commit is contained in:
Pablo Fernandez Guerra (PFE)
2026-04-29 10:27:30 +02:00
committed by GitHub
parent be3c5fb3c1
commit 13b04d339b
4 changed files with 295 additions and 0 deletions
+7
View File
@@ -113,6 +113,13 @@ export default defineConfig({
name: "sign-up",
testMatch: "sign-up.spec.ts",
},
// This project runs the invitation accept smart router test suite
// Tests run unauthenticated (no auth setup dependency)
{
name: "invitation-accept",
use: { ...devices["Desktop Chrome"] },
testMatch: /invitation-accept\/.*\.spec\.ts/,
},
// This project runs the scans test suite
{
name: "scans",
@@ -0,0 +1,62 @@
import { expect, Locator, Page } from "@playwright/test";
import { BasePage } from "../base-page";
export class InvitationAcceptPage extends BasePage {
// Choice screen (unauthenticated user with valid token)
readonly choiceHeading: Locator;
readonly choiceDescription: Locator;
readonly signInButton: Locator;
readonly createAccountButton: Locator;
// No-token error screen
readonly noTokenHeading: Locator;
readonly noTokenDescription: Locator;
readonly goToSignInLink: Locator;
constructor(page: Page) {
super(page);
this.choiceHeading = page.getByRole("heading", {
name: "You've Been Invited",
});
this.choiceDescription = page.getByText(/invited to join a tenant/i);
this.signInButton = page.getByRole("button", {
name: /I have an account.*Sign in/i,
});
this.createAccountButton = page.getByRole("button", {
name: /I'm new.*Create an account/i,
});
this.noTokenHeading = page.getByRole("heading", {
name: "Invalid Invitation Link",
});
this.noTokenDescription = page.getByText(
/No invitation token was provided/i,
);
this.goToSignInLink = page.getByRole("link", { name: "Go to Sign In" });
}
async gotoWithToken(token: string): Promise<void> {
await super.goto(
`/invitation/accept?invitation_token=${encodeURIComponent(token)}`,
);
}
async gotoWithoutToken(): Promise<void> {
await super.goto("/invitation/accept");
}
async verifyChoiceScreen(): Promise<void> {
await expect(this.choiceHeading).toBeVisible();
await expect(this.choiceDescription).toBeVisible();
await expect(this.signInButton).toBeVisible();
await expect(this.createAccountButton).toBeVisible();
}
async verifyNoTokenScreen(): Promise<void> {
await expect(this.noTokenHeading).toBeVisible();
await expect(this.noTokenDescription).toBeVisible();
await expect(this.goToSignInLink).toBeVisible();
}
}
@@ -0,0 +1,131 @@
### E2E Tests: Invitation Accept Smart Router
**Suite ID:** `INVITE-ACCEPT-E2E`
**Feature:** `/invitation/accept` smart router that handles invitation links for both
authenticated and unauthenticated users.
---
## Test Case: `INVITE-ACCEPT-E2E-001` - Unauthenticated user sees choice screen and Sign in preserves token
**Priority:** `high`
**Tags:**
- type: @e2e
- feature: @invitation, @invitation-accept
**Description/Objective:** Verify the smart router renders the choice screen for
an unauthenticated user with a valid token, and that clicking "Sign in"
redirects to `/sign-in` with a `callbackUrl` that preserves the invitation token.
**Preconditions:**
- Application is running.
- No active session (cookies cleared).
### Flow Steps:
1. Clear all cookies.
2. Navigate to `/invitation/accept?invitation_token=test-token`.
3. Verify the choice screen is rendered.
4. Click the "I have an account — Sign in" button.
5. Verify the redirect target and `callbackUrl` query param.
### Expected Result:
- Heading "You've Been Invited" is visible.
- Description text "invited to join a tenant" is visible.
- Both "I have an account — Sign in" and "I'm new — Create an account" buttons are visible.
- After clicking "Sign in", URL is `/sign-in?callbackUrl=...`.
- Decoded `callbackUrl` equals `/invitation/accept?invitation_token=test-token`.
- Decoded `callbackUrl` contains `invitation_token=test-token`.
### Key verification points:
- `callbackUrl` preserves the original invitation path with token.
---
## Test Case: `INVITE-ACCEPT-E2E-002` - "Create an account" button redirects to sign-up with invitation token
**Priority:** `high`
**Tags:**
- type: @e2e
- feature: @invitation, @invitation-accept
**Description/Objective:** Verify the "Create an account" button redirects to
`/sign-up` preserving the `invitation_token` query param, and that the sign-up
form actually renders (no redirect loop back to `/invitation/accept`).
**Preconditions:**
- Application is running.
- No active session (cookies cleared).
### Flow Steps:
1. Clear all cookies.
2. Navigate to `/invitation/accept?invitation_token=test-token`.
3. Wait for the "I'm new — Create an account" button to be visible.
4. Click the button.
5. Verify the resulting URL and that the sign-up form is rendered.
### Expected Result:
- URL pathname is `/sign-up`.
- Query param `invitation_token` equals `test-token`.
- Sign-up form is rendered (email input and submit button visible).
### Key verification points:
- No redirect back to `/invitation/accept` (smart router does not loop).
### Notes:
- The legacy `action=signup` param is no longer emitted: the backward-compat
redirect from `/sign-up?invitation_token=...` to `/invitation/accept` was
removed, so no action bypass is needed. See also `AUTH-MW-E2E-003` in
`ui/tests/auth/auth-middleware.spec.ts`, which covers that `/sign-up` with a
token is no longer rewritten.
---
## Test Case: `INVITE-ACCEPT-E2E-004` - No token shows error screen
**Priority:** `medium`
**Tags:**
- type: @e2e
- feature: @invitation, @invitation-accept
**Description/Objective:** Verify that navigating to `/invitation/accept`
without an `invitation_token` query param shows the no-token error state and
that the "Go to Sign In" link redirects to `/sign-in`.
**Preconditions:**
- Application is running.
- No active session (cookies cleared).
### Flow Steps:
1. Clear all cookies.
2. Navigate to `/invitation/accept` (no query params).
3. Verify the no-token error screen is rendered.
4. Click the "Go to Sign In" link.
5. Verify redirect to `/sign-in`.
### Expected Result:
- Heading "Invalid Invitation Link" is visible.
- Description "No invitation token was provided" is visible.
- "Go to Sign In" link is visible and clickable.
- After click, URL matches `/sign-in`.
### Key verification points:
- Client-side render only: no API calls involved.
@@ -0,0 +1,95 @@
import { expect, test } from "@playwright/test";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
import { SignUpPage } from "../sign-up/sign-up-page";
import { InvitationAcceptPage } from "./invitation-accept-page";
test.describe("Invitation Accept Smart Router", () => {
// Match the auth suites' timeout to handle slow dev server starts
test.setTimeout(60000);
test(
"unauthenticated user sees choice screen and Sign in preserves token in callbackUrl",
{
tag: [
"@e2e",
"@invitation",
"@invitation-accept",
"@INVITE-ACCEPT-E2E-001",
],
},
async ({ page, context }) => {
const invitationPage = new InvitationAcceptPage(page);
const signInPage = new SignInPage(page);
await context.clearCookies();
const token = "test-token";
await invitationPage.gotoWithToken(token);
await invitationPage.verifyChoiceScreen();
await invitationPage.signInButton.click();
await signInPage.verifyRedirectWithCallback(
`/invitation/accept?invitation_token=${token}`,
);
const callbackUrl = new URL(page.url()).searchParams.get("callbackUrl");
expect(callbackUrl).toContain(`invitation_token=${token}`);
},
);
test(
'"Create an account" button redirects to sign-up with the invitation token',
{
tag: [
"@e2e",
"@invitation",
"@invitation-accept",
"@INVITE-ACCEPT-E2E-002",
],
},
async ({ page, context }) => {
const invitationPage = new InvitationAcceptPage(page);
const signUpPage = new SignUpPage(page);
await context.clearCookies();
const token = "test-token";
await invitationPage.gotoWithToken(token);
await expect(invitationPage.createAccountButton).toBeVisible();
await invitationPage.createAccountButton.click();
await page.waitForURL(/\/sign-up\?/);
const url = new URL(page.url());
expect(url.pathname).toBe("/sign-up");
expect(url.searchParams.get("invitation_token")).toBe(token);
await signUpPage.verifyPageLoaded();
},
);
test(
"navigating to /invitation/accept without a token shows the no-token error screen",
{
tag: [
"@e2e",
"@invitation",
"@invitation-accept",
"@INVITE-ACCEPT-E2E-004",
],
},
async ({ page, context }) => {
const invitationPage = new InvitationAcceptPage(page);
const signInPage = new SignInPage(page);
await context.clearCookies();
await invitationPage.gotoWithoutToken();
await invitationPage.verifyNoTokenScreen();
await invitationPage.goToSignInLink.click();
await signInPage.verifyOnSignInPage();
},
);
});