From 710f1a865ce8a6fbe34bb197d100e4c9c6c7676e Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 21 May 2026 14:48:32 +0900 Subject: [PATCH] test verify-only approval close routing --- userfront-e2e/tests/auth-routing.spec.ts | 222 ++++++++++++++++++++++- 1 file changed, 219 insertions(+), 3 deletions(-) diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts index abd2f5a8..ca6b30ab 100644 --- a/userfront-e2e/tests/auth-routing.spec.ts +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -3,6 +3,8 @@ import { expect, test, type Page, type Route } from '@playwright/test'; type MockOptions = { sessionStatus?: number; captureApprove?: (pendingRef: string | null) => void; + captureUserMe?: () => void; + captureVerify?: (path: string, body: Record) => void; }; async function seedTokenLogin(page: Page): Promise { @@ -33,11 +35,12 @@ async function mockUserfrontApis( ): Promise { const sessionStatus = options.sessionStatus ?? 200; - await page.route('**/api/v1/**', async (route: Route) => { + await page.context().route('**/api/v1/**', async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; if (path.endsWith('/api/v1/user/me')) { + options.captureUserMe?.(); if (sessionStatus === 200) { await route.fulfill({ status: 200, @@ -117,6 +120,13 @@ async function mockUserfrontApis( path.endsWith('/api/v1/auth/login/code/verify') || path.endsWith('/api/v1/auth/login/code/verify-short') ) { + let body: Record = {}; + try { + body = (route.request().postDataJSON() ?? {}) as Record; + } catch { + body = {}; + } + options.captureVerify?.(path, body); await route.fulfill({ status: 200, contentType: 'application/json', @@ -196,13 +206,35 @@ test.describe('UserFront WASM auth routing', () => { test('verifyOnly 승인 완료 화면의 상단 액션은 signin으로 이동시키지 않는다', async ({ page, }) => { - await mockUserfrontApis(page, { sessionStatus: 401 }); + let userMeCalls = 0; + const verifyRequests: Array<{ + path: string; + body: Record; + }> = []; + + await mockUserfrontApis(page, { + sessionStatus: 401, + captureUserMe: () => { + userMeCalls += 1; + }, + captureVerify: (path, body) => { + verifyRequests.push({ path, body }); + }, + }); await page.goto('/ko/l/AB123456'); await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); - await page.waitForTimeout(500); + await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); + expect(userMeCalls).toBe(0); + expect(verifyRequests[0].path).toContain( + '/api/v1/auth/login/code/verify-short', + ); + expect(verifyRequests[0].body).toMatchObject({ + shortCode: 'AB123456', + verifyOnly: true, + }); await page.locator('flt-glass-pane').click({ position: { x: 30, y: 28 }, @@ -212,4 +244,188 @@ test.describe('UserFront WASM auth routing', () => { await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); }); + + test('verifyOnly 승인 완료 버튼은 SMS 링크에서 user/me 조회나 루트 이동을 만들지 않는다', async ({ + page, + }) => { + let userMeCalls = 0; + let verifyCalls = 0; + + await mockUserfrontApis(page, { + sessionStatus: 401, + captureUserMe: () => { + userMeCalls += 1; + }, + captureVerify: () => { + verifyCalls += 1; + }, + }); + + await page.goto('/ko/l/AB123456'); + + await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); + await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1); + expect(userMeCalls).toBe(0); + + const viewport = page.viewportSize(); + if (!viewport) throw new Error('viewport is required'); + await page.locator('flt-glass-pane').click({ + position: { + x: Math.floor(viewport.width / 2), + y: Math.floor(viewport.height * 0.66), + }, + force: true, + }); + await page.waitForTimeout(300); + + expect(userMeCalls).toBe(0); + await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); + }); + + test('verifyOnly 승인 링크를 팝업에서 닫으면 창만 닫히고 부모는 이동하지 않는다', async ({ + page, + }, testInfo) => { + let userMeCalls = 0; + let verifyCalls = 0; + + await mockUserfrontApis(page, { + sessionStatus: 401, + captureUserMe: () => { + userMeCalls += 1; + }, + captureVerify: () => { + verifyCalls += 1; + }, + }); + + const baseURL = testInfo.project.use.baseURL; + if (typeof baseURL !== 'string') throw new Error('baseURL is required'); + const popupURL = new URL('/ko/l/AB123456', baseURL).toString(); + + await page.goto('about:blank'); + await expect(page).toHaveURL('about:blank'); + + const popupPromise = page.waitForEvent('popup'); + await page.evaluate((url) => { + window.open(url, '_blank'); + }, popupURL); + const popup = await popupPromise; + + await expect(popup).toHaveURL(/\/ko\/l\/AB123456$/); + await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1); + expect(userMeCalls).toBe(0); + + const viewport = popup.viewportSize(); + if (!viewport) throw new Error('viewport is required'); + const closePromise = popup.waitForEvent('close'); + await popup.locator('flt-glass-pane').click({ + position: { + x: Math.floor(viewport.width / 2), + y: Math.floor(viewport.height * 0.66), + }, + force: true, + }); + await closePromise; + + expect(userMeCalls).toBe(0); + await expect(page).toHaveURL('about:blank'); + }); + + test('verifyOnly 승인 완료 버튼은 이메일 magic link에서도 user/me 조회나 루트 이동을 만들지 않는다', async ({ + page, + }) => { + let userMeCalls = 0; + const verifyRequests: Array<{ + path: string; + body: Record; + }> = []; + + await mockUserfrontApis(page, { + sessionStatus: 401, + captureUserMe: () => { + userMeCalls += 1; + }, + captureVerify: (path, body) => { + verifyRequests.push({ path, body }); + }, + }); + + await page.goto('/ko/verify/e2e-email-token'); + + await expect(page).toHaveURL(/\/ko\/verify\/e2e-email-token$/); + await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); + expect(userMeCalls).toBe(0); + expect(verifyRequests[0].path).toContain('/api/v1/auth/magic-link/verify'); + expect(verifyRequests[0].body).toMatchObject({ + token: 'e2e-email-token', + verifyOnly: true, + }); + + const viewport = page.viewportSize(); + if (!viewport) throw new Error('viewport is required'); + await page.locator('flt-glass-pane').click({ + position: { + x: Math.floor(viewport.width / 2), + y: Math.floor(viewport.height * 0.66), + }, + force: true, + }); + await page.waitForTimeout(300); + + expect(userMeCalls).toBe(0); + await expect(page).toHaveURL(/\/ko\/verify\/e2e-email-token$/); + }); + + test('verifyOnly 승인 완료 버튼은 이메일 code link에서도 user/me 조회나 루트 이동을 만들지 않는다', async ({ + page, + }) => { + let userMeCalls = 0; + const verifyRequests: Array<{ + path: string; + body: Record; + }> = []; + + await mockUserfrontApis(page, { + sessionStatus: 401, + captureUserMe: () => { + userMeCalls += 1; + }, + captureVerify: (path, body) => { + verifyRequests.push({ path, body }); + }, + }); + + await page.goto( + '/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email', + ); + + await expect(page).toHaveURL( + /\/ko\/verify\?loginId=e2e(?:%40|@)example\.com&code=654321&pendingRef=pending-email$/, + ); + await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); + expect(userMeCalls).toBe(0); + expect(verifyRequests[0].path).toContain('/api/v1/auth/login/code/verify'); + expect(verifyRequests[0].body).toMatchObject({ + loginId: 'e2e@example.com', + code: '654321', + pendingRef: 'pending-email', + verifyOnly: true, + }); + + const viewport = page.viewportSize(); + if (!viewport) throw new Error('viewport is required'); + await page.locator('flt-glass-pane').click({ + position: { + x: Math.floor(viewport.width / 2), + y: Math.floor(viewport.height * 0.66), + }, + force: true, + }); + await page.waitForTimeout(300); + + expect(userMeCalls).toBe(0); + await expect(page).toHaveURL( + /\/ko\/verify\?loginId=e2e(?:%40|@)example\.com&code=654321&pendingRef=pending-email$/, + ); + }); });