Skip to content

Commit 5999460

Browse files
oriolriusclaude
andcommitted
test(e2e): add production e2e tests for OIDC authentication
Add comprehensive Playwright e2e tests for the production deployment with runtime config.json OIDC authentication: - config.json is served correctly - unauthenticated user redirects to Keycloak - successful login with runtime config - API calls work with authenticated session - tokens stored with runtime config storage key - page reload maintains authentication - navigation works when authenticated - logout works correctly - error handling for invalid credentials Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cda75f6 commit 5999460

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed

tests/e2e-production.spec.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
/**
4+
* E2E Tests for PKI Manager Production Deployment
5+
*
6+
* Tests the complete OIDC authentication flow with runtime config.json
7+
* on the production deployment at pki.nexiona.io.
8+
*
9+
* Prerequisites:
10+
* - PKI Manager deployed at pki.nexiona.io
11+
* - Backend deployed at api.pki.nexiona.io
12+
* - Keycloak running at iam.nexiona.io with pki-manager realm
13+
* - Test user created: testuser / Test123!
14+
*
15+
* Environment variables:
16+
* PROD_FRONTEND_URL - Frontend URL (default: https://pki.nexiona.io)
17+
* PROD_KEYCLOAK_URL - Keycloak URL (default: https://iam.nexiona.io)
18+
* PROD_TEST_USER - Test username (default: testuser)
19+
* PROD_TEST_PASSWORD - Test password (default: Test123!)
20+
*
21+
* Usage:
22+
* pnpm playwright test tests/e2e-production.spec.ts
23+
*/
24+
25+
// Configuration from environment
26+
const FRONTEND_URL = process.env.PROD_FRONTEND_URL || 'https://pki.nexiona.io';
27+
const KEYCLOAK_URL = process.env.PROD_KEYCLOAK_URL || 'https://iam.nexiona.io';
28+
const TEST_USER = process.env.PROD_TEST_USER || 'testuser';
29+
const TEST_PASSWORD = process.env.PROD_TEST_PASSWORD || 'Test123!';
30+
31+
/**
32+
* Helper: Clear all authentication state
33+
*/
34+
async function clearAuthState(page: Page) {
35+
try {
36+
await page.goto(FRONTEND_URL, { waitUntil: 'commit', timeout: 10000 });
37+
} catch {
38+
// Ignore navigation errors during cleanup
39+
}
40+
await page.evaluate(() => {
41+
localStorage.clear();
42+
sessionStorage.clear();
43+
});
44+
}
45+
46+
/**
47+
* Helper: Perform Keycloak login
48+
*/
49+
async function keycloakLogin(page: Page, username: string, password: string) {
50+
// Wait for Keycloak login form
51+
await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible({
52+
timeout: 15000,
53+
});
54+
55+
// Fill credentials
56+
await page.getByRole('textbox', { name: /username or email/i }).fill(username);
57+
await page.getByRole('textbox', { name: /password/i }).fill(password);
58+
59+
// Click sign in
60+
await page.getByRole('button', { name: /sign in/i }).click();
61+
}
62+
63+
/**
64+
* Helper: Wait for app dashboard to be loaded
65+
*/
66+
async function waitForDashboard(page: Page) {
67+
// Wait for navigation back to app
68+
await page.waitForFunction(
69+
() => !window.location.href.includes('iam.nexiona.io'),
70+
{ timeout: 30000 }
71+
);
72+
73+
// Wait for dashboard heading
74+
await expect(
75+
page.getByRole('heading', { name: /own your security infrastructure/i })
76+
).toBeVisible({ timeout: 20000 });
77+
}
78+
79+
test.describe('Production E2E Tests - Runtime OIDC Config', () => {
80+
test.describe.configure({ mode: 'serial' });
81+
82+
test.beforeEach(async ({ page }) => {
83+
await clearAuthState(page);
84+
});
85+
86+
test('config.json is served correctly', async ({ page }) => {
87+
// Fetch config.json directly
88+
const response = await page.request.get(`${FRONTEND_URL}/config.json`);
89+
expect(response.ok()).toBe(true);
90+
91+
const config = await response.json();
92+
expect(config.oidc).toBeDefined();
93+
expect(config.oidc.authority).toContain('iam.nexiona.io');
94+
expect(config.oidc.clientId).toBe('pki-web');
95+
});
96+
97+
test('unauthenticated user is redirected to Keycloak', async ({ page }) => {
98+
await page.goto(FRONTEND_URL);
99+
100+
// Should redirect to Keycloak
101+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, {
102+
timeout: 20000,
103+
});
104+
105+
// Verify on Keycloak login page
106+
await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible();
107+
});
108+
109+
test('successful login with runtime config', async ({ page }) => {
110+
// Navigate to app (will redirect to Keycloak)
111+
await page.goto(FRONTEND_URL);
112+
113+
// Wait for Keycloak redirect
114+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, {
115+
timeout: 20000,
116+
});
117+
118+
// Perform login
119+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
120+
121+
// Wait for redirect back to app
122+
await waitForDashboard(page);
123+
124+
// Verify user is logged in
125+
await expect(page.getByRole('button', { name: /test user/i })).toBeVisible({
126+
timeout: 10000,
127+
});
128+
});
129+
130+
test('API calls work with authenticated session', async ({ page }) => {
131+
// Login first
132+
await page.goto(FRONTEND_URL);
133+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
134+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
135+
await waitForDashboard(page);
136+
137+
// Wait for dashboard data to load
138+
await page.waitForTimeout(3000);
139+
140+
// Verify dashboard stats load without errors
141+
// Should show numbers instead of "Err"
142+
const bodyText = await page.textContent('body');
143+
expect(bodyText).not.toContain('Error loading');
144+
145+
// Check for stat cards (Total CAs, Total Certs)
146+
await expect(page.locator('text=Total CAs')).toBeVisible();
147+
await expect(page.locator('text=Total Certs')).toBeVisible();
148+
});
149+
150+
test('tokens are stored with runtime config storage key', async ({ page }) => {
151+
// Login
152+
await page.goto(FRONTEND_URL);
153+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
154+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
155+
await waitForDashboard(page);
156+
157+
// Check localStorage for tokens with correct storage key
158+
const tokenData = await page.evaluate(() => {
159+
const keys = Object.keys(localStorage);
160+
// Should be stored with runtime config authority and clientId
161+
const oidcKey = keys.find((key) =>
162+
key.includes('oidc.user:') &&
163+
key.includes('iam.nexiona.io') &&
164+
key.includes('pki-web')
165+
);
166+
if (!oidcKey) return null;
167+
const data = localStorage.getItem(oidcKey);
168+
return data ? JSON.parse(data) : null;
169+
});
170+
171+
expect(tokenData).not.toBeNull();
172+
expect(tokenData.access_token).toBeDefined();
173+
expect(tokenData.id_token).toBeDefined();
174+
expect(tokenData.expires_at).toBeDefined();
175+
});
176+
177+
test('page reload maintains authentication', async ({ page }) => {
178+
// Login
179+
await page.goto(FRONTEND_URL);
180+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
181+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
182+
await waitForDashboard(page);
183+
184+
// Verify logged in
185+
await expect(page.getByRole('button', { name: /test user/i })).toBeVisible();
186+
187+
// Reload page
188+
await page.reload();
189+
await page.waitForTimeout(3000);
190+
191+
// Should still be logged in (no redirect to Keycloak)
192+
const url = page.url();
193+
expect(url).not.toContain('iam.nexiona.io');
194+
await expect(page.getByRole('button', { name: /test user/i })).toBeVisible({
195+
timeout: 10000,
196+
});
197+
});
198+
199+
test('navigation works when authenticated', async ({ page }) => {
200+
// Login
201+
await page.goto(FRONTEND_URL);
202+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
203+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
204+
await waitForDashboard(page);
205+
206+
// Navigate to Certificate Authorities
207+
await page.getByRole('link', { name: /certificate authorities/i }).click();
208+
await expect(page).toHaveURL(/\/cas$/);
209+
// CAs page shows table directly without heading - verify Create CA button is visible
210+
await expect(page.getByRole('button', { name: /create ca/i })).toBeVisible({
211+
timeout: 10000,
212+
});
213+
214+
// Navigate to Certificates
215+
await page.getByRole('link', { name: /^certificates$/i }).click();
216+
await expect(page).toHaveURL(/\/certificates$/);
217+
218+
// Navigate back to Dashboard
219+
await page.getByRole('link', { name: /dashboard/i }).click();
220+
await expect(page).toHaveURL(/\/$/);
221+
});
222+
223+
test('logout works correctly', async ({ page }) => {
224+
// Login
225+
await page.goto(FRONTEND_URL);
226+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
227+
await keycloakLogin(page, TEST_USER, TEST_PASSWORD);
228+
await waitForDashboard(page);
229+
230+
// Open profile menu and logout
231+
await page.getByRole('button', { name: /test user/i }).click();
232+
await page.getByRole('button', { name: /logout/i }).click();
233+
234+
// Should redirect to Keycloak logout
235+
await page.waitForURL(/.*protocol\/openid-connect\/logout/, {
236+
timeout: 15000,
237+
});
238+
239+
// Confirm logout if prompted
240+
const logoutButton = page.getByRole('button', { name: /logout/i });
241+
if (await logoutButton.isVisible({ timeout: 3000 }).catch(() => false)) {
242+
await logoutButton.click();
243+
}
244+
245+
// Should redirect back to login
246+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, {
247+
timeout: 15000,
248+
});
249+
});
250+
});
251+
252+
test.describe('Production E2E - Error Handling', () => {
253+
test('shows error for invalid credentials', async ({ page }) => {
254+
await page.goto(FRONTEND_URL);
255+
await page.waitForURL(/.*iam\.nexiona\.io.*realms.*/, { timeout: 20000 });
256+
257+
// Try login with invalid credentials
258+
await page.getByRole('textbox', { name: /username or email/i }).fill('invaliduser');
259+
await page.getByRole('textbox', { name: /password/i }).fill('wrongpassword');
260+
await page.getByRole('button', { name: /sign in/i }).click();
261+
262+
// Should show error on Keycloak
263+
await expect(page.getByText(/invalid username or password/i)).toBeVisible({
264+
timeout: 5000,
265+
});
266+
267+
// Should still be on Keycloak
268+
expect(page.url()).toContain('iam.nexiona.io');
269+
});
270+
});

0 commit comments

Comments
 (0)