mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(ui): refreshToken implementation (#8864)
This commit is contained in:
@@ -137,8 +137,8 @@ test.describe("Session Persistence", () => {
|
||||
}) => {
|
||||
// Try to access protected route without login
|
||||
await page.goto(URLS.DASHBOARD);
|
||||
// Should be redirected to login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
// Should be redirected to login page (may include callbackUrl)
|
||||
await expect(page).toHaveURL(/\/sign-in/);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ test.describe("Session Persistence", () => {
|
||||
|
||||
// Verify cannot access protected route after logout
|
||||
await page.goto(URLS.DASHBOARD);
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
await expect(page).toHaveURL(/\/sign-in/);
|
||||
});
|
||||
|
||||
test("should handle session timeout gracefully", async ({ browser }) => {
|
||||
@@ -184,8 +184,8 @@ test.describe("Session Persistence", () => {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
|
||||
// Should be redirected to login since this context has no auth
|
||||
await expect(unauthPage).toHaveURL(URLS.LOGIN);
|
||||
// Should be redirected to login since this context has no auth (may include callbackUrl)
|
||||
await expect(unauthPage).toHaveURL(/\/sign-in/);
|
||||
|
||||
// Verify session is null in unauthenticated context
|
||||
const unauthResponse = await unauthPage.request.get("/api/auth/session");
|
||||
|
||||
92
ui/tests/auth-middleware-error.spec.ts
Normal file
92
ui/tests/auth-middleware-error.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
goToLogin,
|
||||
login,
|
||||
verifySuccessfulLogin,
|
||||
verifySessionValid,
|
||||
TEST_CREDENTIALS,
|
||||
URLS,
|
||||
} from "./helpers";
|
||||
|
||||
test.describe("Middleware Error Handling", () => {
|
||||
test("should allow access to public routes without session", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
// Ensure no session exists
|
||||
await context.clearCookies();
|
||||
|
||||
// Try to access login page (public route)
|
||||
await page.goto(URLS.LOGIN);
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
|
||||
// Try to access sign-up page (public route)
|
||||
await page.goto(URLS.SIGNUP);
|
||||
await expect(page).toHaveURL(URLS.SIGNUP);
|
||||
});
|
||||
|
||||
test("should maintain protection after session error", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
// Login
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Navigate to a protected page
|
||||
await page.goto("/providers");
|
||||
await expect(page).toHaveURL("/providers");
|
||||
|
||||
// Simulate session error by corrupting cookie
|
||||
const cookies = await context.cookies();
|
||||
const sessionCookie = cookies.find((c) =>
|
||||
c.name.includes("authjs.session-token"),
|
||||
);
|
||||
|
||||
if (sessionCookie) {
|
||||
await context.clearCookies();
|
||||
await context.addCookies([
|
||||
{
|
||||
...sessionCookie,
|
||||
value: "invalid-session-token",
|
||||
},
|
||||
]);
|
||||
|
||||
// Try to navigate to another protected page
|
||||
await page.goto("/scans", { waitUntil: "networkidle" });
|
||||
|
||||
// Should be redirected to login (may include callbackUrl)
|
||||
await expect(page).toHaveURL(/\/sign-in/);
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle permission-based redirects", async ({ page }) => {
|
||||
// Login with valid credentials
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Get user permissions using helper
|
||||
const session = await verifySessionValid(page);
|
||||
const permissions = session.user.permissions;
|
||||
|
||||
// Test billing route if user doesn't have permission
|
||||
if (!permissions.manage_billing) {
|
||||
await page.goto("/billing", { waitUntil: "networkidle" });
|
||||
|
||||
// Should be redirected to profile (as per middleware logic)
|
||||
await expect(page).toHaveURL("/profile");
|
||||
}
|
||||
|
||||
// Test integrations route if user doesn't have permission
|
||||
if (!permissions.manage_integrations) {
|
||||
await page.goto("/integrations", { waitUntil: "networkidle" });
|
||||
|
||||
// Should be redirected to profile (as per middleware logic)
|
||||
await expect(page).toHaveURL("/profile");
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
124
ui/tests/auth-refresh-token.spec.ts
Normal file
124
ui/tests/auth-refresh-token.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
goToLogin,
|
||||
login,
|
||||
verifySuccessfulLogin,
|
||||
getSession,
|
||||
verifySessionValid,
|
||||
TEST_CREDENTIALS,
|
||||
URLS,
|
||||
} from "./helpers";
|
||||
|
||||
test.describe("Token Refresh Flow", () => {
|
||||
test("should refresh access token when expired", async ({ page }) => {
|
||||
// Login first
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Get initial session using helper
|
||||
const initialSession = await verifySessionValid(page);
|
||||
const initialAccessToken = initialSession.accessToken;
|
||||
|
||||
// Wait for some time to allow token to potentially expire
|
||||
// In a real scenario, you might want to manipulate the token expiry
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Make a request that requires authentication
|
||||
// This should trigger token refresh if needed
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Verify we're still authenticated
|
||||
await expect(page).toHaveURL(URLS.DASHBOARD);
|
||||
|
||||
// Get session after potential refresh using helper
|
||||
const refreshedSession = await verifySessionValid(page);
|
||||
|
||||
// User data should be maintained
|
||||
expect(refreshedSession.user.email).toBe(initialSession.user.email);
|
||||
expect(refreshedSession.userId).toBe(initialSession.userId);
|
||||
expect(refreshedSession.tenantId).toBe(initialSession.tenantId);
|
||||
});
|
||||
|
||||
test("should handle concurrent requests with token refresh", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Login
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Make multiple concurrent requests to the API
|
||||
const requests = Array(5)
|
||||
.fill(null)
|
||||
.map(() => page.request.get("/api/auth/session"));
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
// All requests should succeed - verify using helper
|
||||
for (const response of responses) {
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const session = await response.json();
|
||||
|
||||
// Validate session structure
|
||||
expect(session).toBeTruthy();
|
||||
expect(session.user).toBeTruthy();
|
||||
expect(session.accessToken).toBeTruthy();
|
||||
expect(session.refreshToken).toBeTruthy();
|
||||
expect(session.error).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should preserve user permissions after token refresh", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Login
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Get initial session with permissions using helper
|
||||
const initialSession = await verifySessionValid(page);
|
||||
const initialPermissions = initialSession.user.permissions;
|
||||
|
||||
// Reload page to potentially trigger token refresh
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Get session after reload using helper
|
||||
const refreshedSession = await verifySessionValid(page);
|
||||
|
||||
// Permissions should be preserved
|
||||
expect(refreshedSession.user.permissions).toEqual(initialPermissions);
|
||||
|
||||
// All user data should be preserved
|
||||
expect(refreshedSession.user.email).toBe(initialSession.user.email);
|
||||
expect(refreshedSession.user.name).toBe(initialSession.user.name);
|
||||
expect(refreshedSession.user.companyName).toBe(
|
||||
initialSession.user.companyName,
|
||||
);
|
||||
});
|
||||
|
||||
test("should clear session when cookies are removed", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
// Login
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Verify session is valid using helper
|
||||
await verifySessionValid(page);
|
||||
|
||||
// Clear all cookies to simulate complete session expiry
|
||||
await context.clearCookies();
|
||||
|
||||
// Verify session is null after clearing cookies
|
||||
const expiredSession = await getSession(page);
|
||||
expect(expiredSession).toBeNull();
|
||||
|
||||
// Note: Middleware redirect behavior is tested in auth-middleware-error.spec.ts
|
||||
});
|
||||
});
|
||||
102
ui/tests/auth-session-error-message.spec.ts
Normal file
102
ui/tests/auth-session-error-message.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
goToLogin,
|
||||
login,
|
||||
verifySuccessfulLogin,
|
||||
TEST_CREDENTIALS,
|
||||
URLS,
|
||||
} from "./helpers";
|
||||
|
||||
test.describe("Session Error Messages", () => {
|
||||
test("should show RefreshAccessTokenError message", async ({ page }) => {
|
||||
// Navigate to sign-in with RefreshAccessTokenError query param
|
||||
await page.goto("/sign-in?error=RefreshAccessTokenError");
|
||||
|
||||
// Wait for toast notification
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Verify error toast appears
|
||||
const toast = page.locator('[role="status"], [role="alert"]').first();
|
||||
|
||||
const isVisible = await toast.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
const text = await toast.textContent();
|
||||
expect(text).toContain("Session Expired");
|
||||
expect(text).toContain("Please sign in again");
|
||||
}
|
||||
|
||||
// Verify sign-in form is displayed
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
await expect(page.getByLabel("Password")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show MissingRefreshToken error message", async ({ page }) => {
|
||||
// Navigate to sign-in with MissingRefreshToken query param
|
||||
await page.goto("/sign-in?error=MissingRefreshToken");
|
||||
|
||||
// Wait for toast notification
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Verify error toast appears
|
||||
const toast = page.locator('[role="status"], [role="alert"]').first();
|
||||
|
||||
const isVisible = await toast.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
const text = await toast.textContent();
|
||||
expect(text).toContain("Session Error");
|
||||
}
|
||||
|
||||
// Verify sign-in form is displayed
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show generic error for unknown error types", async ({ page }) => {
|
||||
// Navigate to sign-in with unknown error type
|
||||
await page.goto("/sign-in?error=UnknownError");
|
||||
|
||||
// Wait for toast notification
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Verify generic error toast appears
|
||||
const toast = page.locator('[role="status"], [role="alert"]').first();
|
||||
|
||||
const isVisible = await toast.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
const text = await toast.textContent();
|
||||
expect(text).toContain("Authentication Error");
|
||||
expect(text).toContain("Please sign in again");
|
||||
}
|
||||
});
|
||||
|
||||
test("should include callbackUrl in redirect", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
// Login first
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
|
||||
// Navigate to a specific page
|
||||
await page.goto("/scans");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Clear cookies to simulate session expiry
|
||||
await context.clearCookies();
|
||||
|
||||
// Try to navigate to a different protected route
|
||||
await page.goto("/providers");
|
||||
|
||||
// Should be redirected to login with callbackUrl
|
||||
await expect(page).toHaveURL(/\/sign-in\?.*callbackUrl=/);
|
||||
|
||||
// Verify callbackUrl contains the attempted route
|
||||
const url = new URL(page.url());
|
||||
const callbackUrl = url.searchParams.get("callbackUrl");
|
||||
expect(callbackUrl).toBe("/providers");
|
||||
});
|
||||
|
||||
});
|
||||
@@ -100,7 +100,7 @@ export async function logout(page: Page) {
|
||||
}
|
||||
|
||||
export async function verifyLogoutSuccess(page: Page) {
|
||||
await expect(page).toHaveURL("/sign-in");
|
||||
await expect(page).toHaveURL(/\/sign-in/);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -137,3 +137,17 @@ export async function waitForPageLoad(page: Page) {
|
||||
export async function verifyDashboardRoute(page: Page) {
|
||||
await expect(page).toHaveURL("/");
|
||||
}
|
||||
|
||||
export async function getSession(page: Page) {
|
||||
const response = await page.request.get("/api/auth/session");
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function verifySessionValid(page: Page) {
|
||||
const session = await getSession(page);
|
||||
expect(session).toBeTruthy();
|
||||
expect(session.user).toBeTruthy();
|
||||
expect(session.accessToken).toBeTruthy();
|
||||
expect(session.refreshToken).toBeTruthy();
|
||||
return session;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user