feat: add account menu and redirect after Q&A logout
Deploy EG-BIM QA Gateway / deploy (push) Successful in 54s
Deploy EG-BIM QA Gateway / deploy (push) Successful in 54s
Add the top-right account icon with the signed-in name and logout link. Expire the Q&A session before redirecting to the configured inquiry homepage in both environments. Preserve login return URL checks and ignore request-controlled logout destinations. Validation: 106 tests, syntax checks, and mocked Chromium desktop/mobile regressions pass, including session-cookie removal and the destination navigation request with external networking blocked.
This commit is contained in:
1 parent
2b37bed2b8
commit
beaa2fc423
13 files changed
+135
-13
No files matched your search
@@ -6,6 +6,7 @@ import {createRequire} from 'node:module';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {join} from 'node:path';
|
||||
import {tmpdir} from 'node:os';
|
||||
import worker from '../../src/index.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
const {chromium} = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url)).replace(/\/$/, '');
|
||||
@@ -17,7 +18,12 @@ await mkdir(images,{recursive:true});
|
||||
const errors = [];
|
||||
try {
|
||||
const context = await browser.newContext({viewport:{width:1900,height:1000}});
|
||||
await context.addCookies([{name:'fixture_session',value:'existing-session',url:'https://qa.example.test',httpOnly:true,secure:true}]);
|
||||
const page = await context.newPage();page.setDefaultTimeout(8000);page.setDefaultNavigationTimeout(8000);
|
||||
// Redirect continuations bypass Playwright route handlers; block the real destinations in Chromium.
|
||||
const network = await context.newCDPSession(page);
|
||||
await network.send('Network.enable');
|
||||
await network.send('Network.setBlockedURLs',{urls:['https://test.baroncs.co.kr/*','https://sso.hmac.kr/*']});
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
const records = Array.from({length:15},(_,i)=>({id:id(i+1),requester_id:OWNER,requester_name:'송대일',requester_affiliation:'총괄기획&기술개발센터',requester_department:'IS3',title:['PDF 출력 시 이미지 표시 관련 문의','EG-BIM 로그인 오류 문의','모델링 기능 개선 요청'][i%3] + (i>2?' '+(i+1):''),contents:'EG-BIM 이용 중 궁금한 사항을 문의합니다.\n\n이 영역에 문의 내용과 첨부파일, 답변이 표시됩니다.',Category:['ERROR_QNA','GENERAL_QNA','IMPROVEMENT_QNA'][i%3],created_at:'2026-11-02T01:14:00Z',feedback_status:['NEW','RECEIVED','IN_PROGRESS','PENDING','COMPLETED'][i%5],is_secret:i===0,attachments:i===0?[{url:'/fixture/manual.pdf',original_file_name:'화면 설명.pdf'}]:[]}));
|
||||
const comments = new Map(records.map(r=>[r.id,[{id:id(101),author_id:OWNER,author_name:'송대일',content:'확인 부탁드립니다.',created_at:'2026-11-02T02:00:00Z'},{id:id(102),author_id:'other',author_name:'고객지원',content:'접수되었습니다.',created_at:'2026-11-02T02:15:00Z'}]]));
|
||||
@@ -26,6 +32,10 @@ try {
|
||||
await page.route('**/*',async route=>{
|
||||
const request = route.request(); const url = new URL(request.url());
|
||||
if(url.origin!=='https://qa.example.test') return route.abort();
|
||||
if(url.pathname==='/auth/logout') {
|
||||
const response=await worker.fetch(new Request(url),{SESSION_COOKIE_NAME:'fixture_session',AUTH_POST_LOGOUT_REDIRECT_URI:'https://test.baroncs.co.kr/ko/sw-inquiry.html'});
|
||||
return route.fulfill({status:response.status,headers:Object.fromEntries(response.headers),body:''});
|
||||
}
|
||||
const send = (body,status=200)=>route.fulfill({status,contentType:'application/json',body:JSON.stringify(body)});
|
||||
if(url.pathname==='/auth/session') return send({authenticated:true,user:{requesterId:OWNER,userUuid:OWNER,tenantId:id(555),name:'송대일',email:'fixture@example.test',company:'총괄기획&기술개발센터',department:'IS3'}});
|
||||
if(url.pathname.startsWith('/api/')) {
|
||||
@@ -65,6 +75,14 @@ try {
|
||||
await page.waitForFunction(()=>document.querySelector('#resultCount').textContent==='총 15건');
|
||||
assert.equal(await page.locator('#tab-tova').isVisible(),false);assert.equal(await page.locator('#tab-gaia').isVisible(),false);
|
||||
assert.equal(await page.locator('#tab-egbim').evaluate(e=>Math.abs(e.getBoundingClientRect().x+e.getBoundingClientRect().width/2-innerWidth/2)<1),true);assert.equal(await page.locator('.inquiry-remote').getAttribute('href'),'https://939.co.kr/saman/');
|
||||
await page.locator('#accountButton').click();
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),true);
|
||||
assert.equal(await page.locator('#accountPanel [data-auth-name]').textContent(),'송대일');
|
||||
await page.keyboard.press('Escape');
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),false);
|
||||
assert.equal(await page.locator('#accountButton').evaluate(e=>e===document.activeElement),true);
|
||||
await page.locator('#accountButton').click();await page.locator('#inquiry-title').click();
|
||||
assert.equal(await page.locator('#logoutLink').isVisible(),false);
|
||||
await page.screenshot({path:images+'/desktop-list.png',fullPage:true});
|
||||
await page.locator('#query').fill('로그임');await page.locator('#searchForm').evaluate(f=>f.requestSubmit());assert.equal(await page.locator('#qaRows tr').count(),5);
|
||||
await page.locator('#qaRows tr a').first().click();await page.waitForSelector('#detailModal[open]');await close();assert.equal(await page.locator('#query').inputValue(),'로그임');
|
||||
@@ -117,6 +135,13 @@ try {
|
||||
assert.equal(await page.locator('#editButton').isVisible(),false);
|
||||
assert.equal(await page.locator('#commentForm').isVisible(),false);
|
||||
await close();
|
||||
await page.locator('#accountButton').click();
|
||||
assert.equal(await page.locator('#accountPanel').evaluate(e=>e.getBoundingClientRect().right<=innerWidth&&e.getBoundingClientRect().left>=0),true);
|
||||
await page.screenshot({path:images+'/mobile-account.png'});
|
||||
const logoutDestination = page.waitForRequest('https://test.baroncs.co.kr/ko/sw-inquiry.html');
|
||||
await page.locator('#logoutLink').click({noWaitAfter:true});
|
||||
await logoutDestination;
|
||||
assert.equal((await context.cookies('https://qa.example.test')).some(c=>c.name==='fixture_session'),false);
|
||||
assert.deepEqual(errors,[]);console.log(JSON.stringify({result:'PASS',mutations:mutations.length,screenshots:images}));await context.close();
|
||||
for (const scenario of ['owner', 'other', 'same-name-no-id', 'anonymous-with-stale-user', 'session-unavailable', 'delayed-session']) {
|
||||
const context = await browser.newContext();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import worker from '../src/index.js';
|
||||
|
||||
const destination = 'https://test.baroncs.co.kr/ko/sw-inquiry.html';
|
||||
|
||||
for (const configFile of ['wrangler.toml', 'wrangler.production.toml']) {
|
||||
test(configFile + ' logout expires its own session and redirects to the inquiry homepage', async () => {
|
||||
const config = readFileSync(new URL('../' + configFile, import.meta.url), 'utf8');
|
||||
const setting = name => config.match(new RegExp('^' + name + ' = "([^"]+)"', 'm'))[1];
|
||||
const env = {
|
||||
SESSION_COOKIE_NAME: setting('SESSION_COOKIE_NAME'),
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI: setting('AUTH_POST_LOGOUT_REDIRECT_URI')
|
||||
};
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/logout?return_url=https://untrusted.example/&redirect_uri=https://untrusted.example/', {
|
||||
headers: { cookie: env.SESSION_COOKIE_NAME + '=existing-session' }
|
||||
}), env);
|
||||
assert.equal(response.status, 302);
|
||||
assert.equal(response.headers.get('location'), destination);
|
||||
const cookie = response.headers.get('set-cookie');
|
||||
assert.ok(cookie.startsWith(env.SESSION_COOKIE_NAME + '=;'));
|
||||
for (const attribute of ['Max-Age=0', 'Path=/', 'Secure', 'HttpOnly', 'SameSite=Lax', 'Expires=Thu, 01 Jan 1970 00:00:00 GMT']) assert.ok(cookie.includes(attribute));
|
||||
assert.match(response.headers.get('cache-control'), /no-store/);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, value, expected] of [
|
||||
['no configured destination', undefined, '/'],
|
||||
['same-origin relative destination', '/signed-out', '/signed-out'],
|
||||
['invalid URL', 'https://[invalid', '/'],
|
||||
['script URL', 'javascript:alert(1)', '/'],
|
||||
['external insecure URL', 'http://untrusted.example/', '/'],
|
||||
['URL containing credentials', 'https://user:pass@untrusted.example/', '/']
|
||||
]) {
|
||||
test('logout handles ' + label + ' while clearing the session', async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/logout'), {
|
||||
SESSION_COOKIE_NAME: 'fixture_session', AUTH_POST_LOGOUT_REDIRECT_URI: value
|
||||
});
|
||||
assert.equal(response.status, 302);
|
||||
assert.equal(response.headers.get('location'), 'https://qa.example.test' + expected);
|
||||
assert.match(response.headers.get('set-cookie'), /fixture_session=; Max-Age=0/);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user