feat(ui): refreshToken implementation (#8864)

This commit is contained in:
Alejandro Bailo
2025-10-10 11:02:10 +02:00
committed by GitHub
parent ef60ea99c3
commit 046baa8eb9
10 changed files with 618 additions and 117 deletions

View File

@@ -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");

View 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");
}
});
});

View 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
});
});

View 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");
});
});

View File

@@ -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;
}