forked from baron/baron-sso
201 lines
6.5 KiB
TypeScript
201 lines
6.5 KiB
TypeScript
import { expect, test, type BrowserContext, type Page } from '@playwright/test';
|
|
|
|
const USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso-test.hmac.kr';
|
|
const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173';
|
|
const LOGIN_ID = process.env.E2E_LOGIN_ID ?? '';
|
|
const PASSWORD = process.env.E2E_PASSWORD ?? '';
|
|
|
|
type SessionApiResponse = {
|
|
items?: Array<{
|
|
session_id?: string;
|
|
client_id?: string;
|
|
app_name?: string;
|
|
is_current?: boolean;
|
|
user_agent?: string;
|
|
ip_address?: string;
|
|
}>;
|
|
};
|
|
|
|
function ensureCredentials(): void {
|
|
if (!LOGIN_ID || !PASSWORD) {
|
|
test.skip(true, 'E2E credentials are required');
|
|
}
|
|
}
|
|
|
|
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
|
await page.waitForTimeout(300);
|
|
const button = page.getByRole('button', { name: 'Enable accessibility' });
|
|
if (await button.count()) {
|
|
try {
|
|
await button.click({ force: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
const placeholder = page.locator('flt-semantics-placeholder');
|
|
if (await placeholder.count()) {
|
|
await placeholder.first().click({ force: true });
|
|
}
|
|
await page.waitForTimeout(800);
|
|
}
|
|
}
|
|
|
|
async function clickPasswordTab(page: Page): Promise<void> {
|
|
await page.waitForTimeout(900);
|
|
const pane = page.locator('flt-glass-pane');
|
|
await pane.click({
|
|
position: { x: 522, y: 158 },
|
|
force: true,
|
|
});
|
|
await page.waitForTimeout(120);
|
|
await pane.click({
|
|
position: { x: 522, y: 158 },
|
|
force: true,
|
|
});
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
async function fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
|
|
const pane = page.locator('flt-glass-pane');
|
|
await pane.click({ position: { x, y }, force: true });
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.press('Control+A');
|
|
await page.keyboard.press('Backspace');
|
|
await page.keyboard.type(value);
|
|
}
|
|
|
|
async function loginViaUserFront(page: Page): Promise<void> {
|
|
await page.waitForURL(/\/ko\/(signin|login)/, { timeout: 30_000 });
|
|
const loginIdInput = page.getByPlaceholder(/이메일 또는 휴대폰 번호|email|phone/i);
|
|
const passwordInput = page.getByPlaceholder(/비밀번호|password/i);
|
|
const submitButton = page.getByRole('button', { name: /로그인|Login/i });
|
|
|
|
if ((await loginIdInput.count()) >= 1 && (await passwordInput.count()) >= 1) {
|
|
await loginIdInput.first().fill(LOGIN_ID);
|
|
await passwordInput.first().fill(PASSWORD);
|
|
await submitButton.click();
|
|
return;
|
|
}
|
|
|
|
await clickPasswordTab(page);
|
|
await fillAt(page, 640, 245, LOGIN_ID);
|
|
await fillAt(page, 640, 311, PASSWORD);
|
|
await page.locator('flt-glass-pane').click({
|
|
position: { x: 640, y: 381 },
|
|
force: true,
|
|
});
|
|
}
|
|
|
|
async function ensureConsentIfNeeded(page: Page): Promise<void> {
|
|
if (!/\/ko\/consent/.test(page.url())) {
|
|
return;
|
|
}
|
|
|
|
const allowButton = page
|
|
.getByRole('button')
|
|
.filter({ hasText: /허용|동의|Accept|Allow/i })
|
|
.first();
|
|
|
|
if (await allowButton.count()) {
|
|
await allowButton.click({ force: true });
|
|
}
|
|
}
|
|
|
|
async function captureUserSessionsOnReload(page: Page): Promise<SessionApiResponse> {
|
|
const responsePromise = page.waitForResponse(
|
|
(response) =>
|
|
response.request().method() === 'GET' &&
|
|
response.url().includes('/api/v1/user/sessions'),
|
|
{ timeout: 30_000 },
|
|
);
|
|
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
const response = await responsePromise;
|
|
return (await response.json()) as SessionApiResponse;
|
|
}
|
|
|
|
async function loginUserFront(context: BrowserContext): Promise<Page> {
|
|
const page = await context.newPage();
|
|
await page.goto(`${USERFRONT_BASE_URL}/ko/signin`, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
await loginViaUserFront(page);
|
|
await expect(page).toHaveURL(/\/ko\/dashboard/, { timeout: 60_000 });
|
|
return page;
|
|
}
|
|
|
|
async function loginAdminFront(context: BrowserContext): Promise<Page> {
|
|
const page = await context.newPage();
|
|
await page.goto(ADMINFRONT_URL, { waitUntil: 'domcontentloaded' });
|
|
const ssoButton = page.getByRole('button', { name: /SSO 계정으로 로그인|SSO/i });
|
|
if (await ssoButton.count()) {
|
|
await ssoButton.click({ force: true });
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
if (/\/login$/.test(page.url())) {
|
|
const authorizeUrl = await page.evaluate(() => {
|
|
const origin = window.location.origin;
|
|
const authority = 'https://sso-test.hmac.kr/oidc';
|
|
const params = new URLSearchParams({
|
|
client_id: 'adminfront',
|
|
redirect_uri: `${origin}/auth/callback`,
|
|
response_type: 'code',
|
|
scope: 'openid offline_access profile email',
|
|
state: `pw-${Date.now()}`,
|
|
nonce: `pw-${Date.now()}`,
|
|
code_challenge: 'test-code-challenge-test-code-challenge-test',
|
|
code_challenge_method: 'plain',
|
|
});
|
|
return `${authority}/oauth2/auth?${params.toString()}`;
|
|
});
|
|
await page.goto(authorizeUrl, { waitUntil: 'domcontentloaded' });
|
|
}
|
|
await loginViaUserFront(page);
|
|
await ensureConsentIfNeeded(page);
|
|
await page.waitForURL(/localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, {
|
|
timeout: 60_000,
|
|
});
|
|
return page;
|
|
}
|
|
|
|
test.describe('cross-browser session debug', () => {
|
|
test('userfront session card should map adminfront session metadata across contexts', async ({
|
|
browser,
|
|
}, testInfo) => {
|
|
ensureCredentials();
|
|
|
|
const userfrontContext = await browser.newContext({ locale: 'ko-KR' });
|
|
const adminfrontContext = await browser.newContext({ locale: 'ko-KR' });
|
|
|
|
const userfrontPage = await loginUserFront(userfrontContext);
|
|
const adminfrontPage = await loginAdminFront(adminfrontContext);
|
|
|
|
const sessionsPayload = await captureUserSessionsOnReload(userfrontPage);
|
|
const items = sessionsPayload.items ?? [];
|
|
const adminfrontItems = items.filter((item) =>
|
|
(item.client_id ?? '').toLowerCase().includes('adminfront'),
|
|
);
|
|
const unknownCards = await userfrontPage.locator('text=세션 정보').allTextContents();
|
|
const adminFrontCards = await userfrontPage.locator('text=AdminFront').allTextContents();
|
|
|
|
await testInfo.attach('user-sessions.json', {
|
|
body: JSON.stringify(sessionsPayload, null, 2),
|
|
contentType: 'application/json',
|
|
});
|
|
await testInfo.attach('card-summary.json', {
|
|
body: JSON.stringify(
|
|
{
|
|
unknownCards,
|
|
adminFrontCards,
|
|
currentUrl: userfrontPage.url(),
|
|
adminfrontUrl: adminfrontPage.url(),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
contentType: 'application/json',
|
|
});
|
|
|
|
expect(adminfrontItems.length).toBeGreaterThan(0);
|
|
});
|
|
});
|