generated from ttipo/project-base
feat(frontend): 로그인된 기기 — 앱 세션 · 앱 로그인 기록을 보인다
앱 로그인 서버가 생겨 계정 보안 화면에 앱 세션 · 앱 로그인이 함께 들어온다. - "로그인된 브라우저" → "로그인된 기기"(계약 5절), 개수는 세션의 client 로 "웹메일 2곳 · ARA SYSTEM 앱 1곳"(0곳인 쪽은 쓰지 않음), 앱 세션은 늘 휴대폰 아이콘 - 로그인 기록 방식 "ARA SYSTEM 앱", 앱 줄의 기기 칸은 운영체제만(방식 칸이 이미 앱 이름) - [다른 곳 모두 로그아웃] · 비밀번호 바꾸기 · 안내 글이 웹메일만이 아니라 앱까지 끊는다고 말한다 - 웹 · 앱은 client · method 로만 가른다(User-Agent 이름으로 짐작하지 않는다) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,11 @@ import { createTestRouter } from '@/test-utils/router'
|
||||
import AccountLoginHistoryPage from './AccountLoginHistoryPage.vue'
|
||||
|
||||
// 설정 › 계정 › 로그인 기록 (12단계 — 디자인 원본 SettingsLoginHistory · 계약 5절 "로그인된 브라우저" · "로그인 기록")
|
||||
// 로그인된 기기 · 최근 로그인에는 ARA SYSTEM 앱의 세션 · 로그인도 섞여 온다 (계약 1절 "앱 로그인" — 세션의 client · 기록의 method 가 app)
|
||||
|
||||
const USER = { email: 'ara.kim@arasystem.it.kr', name: '김아라' }
|
||||
/** 서버가 앱 세션 · 앱 로그인 기록에 싣는 기기 */
|
||||
const APP_ANDROID = { browser: 'ARA SYSTEM 앱', os: 'Android' }
|
||||
/**
|
||||
* 2026년 그 시각을 이 PC 의 시간대(지역 시각)로 — 화면은 시각을 브라우저 시간대로 보인다.
|
||||
* "오늘 오전 9:05" 같은 글을 그대로 보려면 시각을 지역 시각으로 적어야 어느 시간대에서도 같다
|
||||
@@ -32,6 +35,7 @@ const SESSIONS: AuthSession[] = [
|
||||
lastActiveAt: local(10, 31, 11, 50),
|
||||
ip: '211.186.95.252',
|
||||
device: { browser: 'Chrome', os: 'Windows' },
|
||||
client: 'web',
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
@@ -40,6 +44,7 @@ const SESSIONS: AuthSession[] = [
|
||||
lastActiveAt: local(10, 31, 9, 5),
|
||||
ip: '211.186.95.252',
|
||||
device: { browser: 'Edge', os: 'Windows' },
|
||||
client: 'web',
|
||||
isCurrent: false,
|
||||
},
|
||||
{
|
||||
@@ -48,6 +53,16 @@ const SESSIONS: AuthSession[] = [
|
||||
lastActiveAt: local(10, 30, 22, 31),
|
||||
ip: null,
|
||||
device: { browser: 'Safari', os: 'iOS' },
|
||||
client: 'web',
|
||||
isCurrent: false,
|
||||
},
|
||||
{
|
||||
id: 'app',
|
||||
createdAt: local(10, 28, 8),
|
||||
lastActiveAt: local(10, 30, 20, 14),
|
||||
ip: '58.29.11.4',
|
||||
device: APP_ANDROID,
|
||||
client: 'app',
|
||||
isCurrent: false,
|
||||
},
|
||||
]
|
||||
@@ -76,6 +91,13 @@ const EVENTS: LoginEvent[] = [
|
||||
device: null,
|
||||
appPassword: { id: 'b', name: '회사 PC 아웃룩' },
|
||||
}),
|
||||
loginEvent('app1', {
|
||||
occurredAt: local(10, 31, 8, 47),
|
||||
method: 'app',
|
||||
device: APP_ANDROID,
|
||||
ip: '58.29.11.4',
|
||||
isTwoFactor: true,
|
||||
}),
|
||||
loginEvent('e4', {
|
||||
occurredAt: local(10, 30, 23, 58),
|
||||
method: 'smtp',
|
||||
@@ -124,7 +146,7 @@ describe('AccountLoginHistoryPage', () => {
|
||||
if (request.url === '/auth/sessions' && request.method === 'GET') {
|
||||
return server.sessions ? server.sessions() : ok(pageOf(SESSIONS))
|
||||
}
|
||||
if (request.url === '/auth/sessions') return server.endOthers ? server.endOthers() : ok({ ended: 2 })
|
||||
if (request.url === '/auth/sessions') return server.endOthers ? server.endOthers() : ok({ ended: 3 })
|
||||
if (request.url.startsWith('/auth/sessions/')) return server.endSession ? server.endSession(request) : ok(null)
|
||||
if (request.url === '/login-events') return server.events ? server.events(request) : ok(pageOf(EVENTS))
|
||||
return ok(null)
|
||||
@@ -166,15 +188,16 @@ describe('AccountLoginHistoryPage', () => {
|
||||
])
|
||||
})
|
||||
|
||||
describe('로그인된 브라우저', () => {
|
||||
it('"웹메일 3곳" · 줄마다 PC · 휴대폰 아이콘 · 브라우저 · 운영체제 / IP / 마지막 활동 — 지금 이 브라우저는 단추 없이 글자만', async () => {
|
||||
describe('로그인된 기기', () => {
|
||||
it('"웹메일 3곳 · ARA SYSTEM 앱 1곳" · 줄마다 PC · 휴대폰 아이콘 · 기기 / IP / 마지막 활동 — 지금 이 브라우저는 단추 없이 글자만', async () => {
|
||||
const wrapper = await mountPage()
|
||||
const nowDate = new Date(NOW)
|
||||
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 3곳')
|
||||
expect(wrapper.get('#login-sessions-title').text()).toBe('로그인된 기기')
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 3곳 · ARA SYSTEM 앱 1곳')
|
||||
expect(wrapper.findAll('thead').at(0)?.findAll('th').map((cell) => cell.text())).toEqual([
|
||||
'종류',
|
||||
'기기',
|
||||
'브라우저 · 운영체제',
|
||||
'IP',
|
||||
'마지막 활동',
|
||||
'로그아웃',
|
||||
@@ -183,20 +206,64 @@ describe('AccountLoginHistoryPage', () => {
|
||||
['Chrome · Windows', '211.186.95.252', '지금 이 브라우저'],
|
||||
['Edge · Windows', '211.186.95.252', formatRelativeDayTime(SESSIONS[1]?.lastActiveAt ?? '', nowDate)],
|
||||
['Safari · iOS', '—알 수 없음', formatRelativeDayTime(SESSIONS[2]?.lastActiveAt ?? '', nowDate)],
|
||||
['ARA SYSTEM 앱 · Android', '58.29.11.4', formatRelativeDayTime(SESSIONS[3]?.lastActiveAt ?? '', nowDate)],
|
||||
])
|
||||
expect(sessionRows(wrapper)[1]?.[2]).toBe('오늘 오전 9:05')
|
||||
expect(wrapper.findAll('[data-session] svg').map((icon) => icon.attributes('aria-label'))).toEqual(['PC', 'PC', '휴대폰'])
|
||||
expect(sessionRows(wrapper)[3]?.[2]).toBe('어제 오후 8:14')
|
||||
expect(wrapper.findAll('[data-session] svg').map((icon) => icon.attributes('aria-label'))).toEqual([
|
||||
'PC',
|
||||
'PC',
|
||||
'휴대폰',
|
||||
'휴대폰',
|
||||
])
|
||||
expect(wrapper.get('[data-session="now"]').find('[data-session-end]').exists()).toBe(false)
|
||||
expect(wrapper.findAll('[data-session-end]').map((button) => button.attributes('aria-label'))).toEqual([
|
||||
'‘Edge · Windows’ 로그아웃',
|
||||
'‘Safari · iOS’ 로그아웃',
|
||||
'‘ARA SYSTEM 앱 · Android’ 로그아웃',
|
||||
])
|
||||
expect(wrapper.get('[data-sessions-end-others]').text()).toBe('다른 곳 모두 로그아웃')
|
||||
const note = wrapper.get('[data-sessions-note]')
|
||||
expect(note.text()).toBe('웹메일 로그인만 보입니다. 메일 프로그램의 연결을 끊으려면 앱 비밀번호를 지워 주세요.')
|
||||
expect(note.text()).toBe('메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요.')
|
||||
expect(note.get('a').attributes('href')).toBe('/settings/account/app-passwords')
|
||||
})
|
||||
|
||||
it('앱 줄은 client 로 정한다 — 운영체제를 몰라도 휴대폰 · 앱 이름, 앱 꼴 웹 세션은 웹메일로 센다', async () => {
|
||||
const wrapper = await mountPage({
|
||||
sessions: () =>
|
||||
ok(
|
||||
pageOf<AuthSession>([
|
||||
SESSIONS[0] as AuthSession,
|
||||
{ ...(SESSIONS[3] as AuthSession), id: 'app-unknown-os', device: { browser: 'ARA SYSTEM 앱', os: null } },
|
||||
// 웹 세션이 앱 꼴 User-Agent 를 보냈다 — 서버는 브라우저를 모른다(null)고 준다
|
||||
{ ...(SESSIONS[1] as AuthSession), id: 'web-android', device: { browser: null, os: 'Android' } },
|
||||
]),
|
||||
),
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 2곳 · ARA SYSTEM 앱 1곳')
|
||||
expect(wrapper.get('[data-session="app-unknown-os"] [data-session-device]').text()).toBe('ARA SYSTEM 앱')
|
||||
expect(wrapper.get('[data-session="app-unknown-os"] svg').attributes('aria-label')).toBe('휴대폰')
|
||||
expect(wrapper.get('[data-session="web-android"] [data-session-device]').text()).toBe('알 수 없는 브라우저 · Android')
|
||||
})
|
||||
|
||||
it('지금 이 브라우저와 앱 세션 하나면 "웹메일 1곳 · ARA SYSTEM 앱 1곳" — 앱 줄을 끊으면 "웹메일 1곳" · 다른 곳에 로그인된 기기가 없다는 안내', async () => {
|
||||
const wrapper = await mountPage({ sessions: () => ok(pageOf([SESSIONS[0] as AuthSession, SESSIONS[3] as AuthSession])) })
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 1곳 · ARA SYSTEM 앱 1곳')
|
||||
|
||||
await wrapper.get('[data-session="app"] [data-session-end]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(requests.filter((request) => request.method === 'DELETE').map((request) => request.url)).toEqual([
|
||||
'/auth/sessions/app',
|
||||
])
|
||||
expect(toastMessages()).toEqual(['‘ARA SYSTEM 앱 · Android’에서 로그아웃했습니다.'])
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 1곳')
|
||||
expect(wrapper.get('[data-sessions-note]').text()).toBe(
|
||||
'다른 곳에 로그인된 기기가 없습니다. 메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요.',
|
||||
)
|
||||
})
|
||||
|
||||
it('모르는 브라우저는 "알 수 없는 브라우저"', async () => {
|
||||
const unknown: AuthSession = { ...SESSIONS[1], id: 'old', ip: null, device: null } as AuthSession
|
||||
const wrapper = await mountPage({ sessions: () => ok(pageOf([SESSIONS[0] as AuthSession, unknown])) })
|
||||
@@ -215,8 +282,24 @@ describe('AccountLoginHistoryPage', () => {
|
||||
'/auth/sessions/edge',
|
||||
])
|
||||
expect(toastMessages()).toEqual(['‘Edge · Windows’에서 로그아웃했습니다.'])
|
||||
expect(sessionRows(wrapper).map((row) => row[0])).toEqual(['Chrome · Windows', 'Safari · iOS'])
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 2곳')
|
||||
expect(sessionRows(wrapper).map((row) => row[0])).toEqual([
|
||||
'Chrome · Windows',
|
||||
'Safari · iOS',
|
||||
'ARA SYSTEM 앱 · Android',
|
||||
])
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 2곳 · ARA SYSTEM 앱 1곳')
|
||||
expect(document.activeElement?.getAttribute('aria-label')).toBe('‘Safari · iOS’ 로그아웃')
|
||||
})
|
||||
|
||||
it('맨 아래 앱 줄을 끊으면 초점은 앞 줄 — 「‘ARA SYSTEM 앱 · Android’에서 로그아웃했습니다.」', async () => {
|
||||
const wrapper = await mountPage()
|
||||
const button = wrapper.get('[data-session="app"] [data-session-end]')
|
||||
;(button.element as HTMLButtonElement).focus()
|
||||
await button.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(toastMessages()).toEqual(['‘ARA SYSTEM 앱 · Android’에서 로그아웃했습니다.'])
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 3곳')
|
||||
expect(document.activeElement?.getAttribute('aria-label')).toBe('‘Safari · iOS’ 로그아웃')
|
||||
})
|
||||
|
||||
@@ -226,10 +309,10 @@ describe('AccountLoginHistoryPage', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(toastMessages()).toEqual(['‘Safari · iOS’에서 로그아웃했습니다.'])
|
||||
expect(sessionRows(wrapper)).toHaveLength(2)
|
||||
expect(sessionRows(wrapper)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('[다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」 · 끊을 곳의 이름) → 「다른 2곳에서 로그아웃했습니다.」 — 지금 이 브라우저만 남는다', async () => {
|
||||
it('[다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」 · 끊을 곳의 이름 — 앱 포함) → 「다른 3곳에서 로그아웃했습니다.」 — 지금 이 브라우저만 남는다', async () => {
|
||||
const wrapper = await mountPage()
|
||||
await wrapper.get('[data-sessions-end-others]').trigger('click')
|
||||
await flushPromises()
|
||||
@@ -238,7 +321,7 @@ describe('AccountLoginHistoryPage', () => {
|
||||
expect(dialog?.hasAttribute('open')).toBe(true)
|
||||
expect(dialog?.getAttribute('role')).toBe('alertdialog')
|
||||
expect(document.querySelector('#end-other-sessions-message')?.textContent?.trim()).toBe(
|
||||
'이 브라우저를 뺀 2곳(Edge · Windows, Safari · iOS)의 웹메일에서 로그아웃합니다. 그곳에서 다음에 무엇이든 누르면 바로 로그인 화면이 나옵니다.',
|
||||
'이 브라우저를 뺀 3곳(Edge · Windows, Safari · iOS, ARA SYSTEM 앱 · Android)에서 로그아웃합니다. 그곳에서 다음에 무엇이든 누르면 바로 로그인 화면이 나옵니다.',
|
||||
)
|
||||
expect(dialog?.textContent).toContain('메일 프로그램은 로그아웃되지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요.')
|
||||
expect(document.activeElement?.hasAttribute('data-end-others-cancel')).toBe(true)
|
||||
@@ -249,12 +332,13 @@ describe('AccountLoginHistoryPage', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(requests.filter((request) => request.method === 'DELETE').map((request) => request.url)).toEqual(['/auth/sessions'])
|
||||
expect(toastMessages()).toEqual(['다른 2곳에서 로그아웃했습니다.'])
|
||||
expect(toastMessages()).toEqual(['다른 3곳에서 로그아웃했습니다.'])
|
||||
expect(dialog?.hasAttribute('open')).toBe(false)
|
||||
expect(sessionRows(wrapper).map((row) => row[0])).toEqual(['Chrome · Windows'])
|
||||
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 1곳')
|
||||
expect(wrapper.find('[data-sessions-end-others]').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-sessions-note]').text()).toBe(
|
||||
'다른 곳에 로그인된 브라우저가 없습니다. 웹메일 로그인만 보입니다. 메일 프로그램의 연결을 끊으려면 앱 비밀번호를 지워 주세요.',
|
||||
'다른 곳에 로그인된 기기가 없습니다. 메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요.',
|
||||
)
|
||||
expect(document.activeElement?.id).toBe('login-sessions-title')
|
||||
})
|
||||
@@ -265,18 +349,18 @@ describe('AccountLoginHistoryPage', () => {
|
||||
// 끊은 뒤 — 이 브라우저의 세션도 끝났다: 다시 받으면 401, 갱신도 401
|
||||
restoreApiAdapter()
|
||||
requests = installMockApi((request) => {
|
||||
if (request.url === '/auth/sessions' && request.method === 'DELETE') return ok({ ended: 3 })
|
||||
if (request.url === '/auth/sessions' && request.method === 'DELETE') return ok({ ended: 4 })
|
||||
return fail(401, 'AUTH_001')
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('[data-session-end]')).toHaveLength(3)
|
||||
expect(wrapper.findAll('[data-session-end]')).toHaveLength(4)
|
||||
await wrapper.get('[data-sessions-end-others]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.querySelector('#end-other-sessions-title')?.textContent?.trim()).toBe(
|
||||
'이 브라우저를 포함해 모든 곳에서 로그아웃할까요?',
|
||||
)
|
||||
expect(document.querySelector('#end-other-sessions-message')?.textContent?.trim()).toBe(
|
||||
'이 브라우저를 알아보지 못해서 이 브라우저를 포함한 3곳(Chrome · Windows, Edge · Windows, Safari · iOS)의 웹메일에서 로그아웃합니다. 이 브라우저도 로그아웃되어 로그인 화면으로 돌아갑니다.',
|
||||
'이 브라우저를 알아보지 못해서 이 브라우저를 포함한 4곳(Chrome · Windows, Edge · Windows, Safari · iOS 외 1곳)에서 로그아웃합니다. 이 브라우저도 로그아웃되어 로그인 화면으로 돌아갑니다.',
|
||||
)
|
||||
|
||||
await wrapper.get('[data-end-others-confirm]').trigger('click')
|
||||
@@ -288,7 +372,7 @@ describe('AccountLoginHistoryPage', () => {
|
||||
'GET /auth/sessions',
|
||||
'POST /auth/refresh',
|
||||
])
|
||||
expect(toastMessages()).toContain('이 브라우저를 포함해 3곳에서 로그아웃했습니다.')
|
||||
expect(toastMessages()).toContain('이 브라우저를 포함해 4곳에서 로그아웃했습니다.')
|
||||
// 빈 목록을 그려 두지 않는다 — 다시 받은 요청이 세션 만료로 이어졌다
|
||||
expect(wrapper.find('[data-session]').exists()).toBe(false)
|
||||
expect(useAuthStore().sessionExpired).toBe(true)
|
||||
@@ -317,35 +401,57 @@ describe('AccountLoginHistoryPage', () => {
|
||||
reply = ok(pageOf(SESSIONS))
|
||||
await wrapper.get('[data-sessions-error] .mail-load-error__action').trigger('click')
|
||||
await flushPromises()
|
||||
expect(sessionRows(wrapper)).toHaveLength(3)
|
||||
expect(sessionRows(wrapper)).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('최근 로그인', () => {
|
||||
it('시각 · 방식(앱 비밀번호 이름) · IP · 기기(메일 프로그램은 —) · 결과 — 실패는 빨간 굵은 글자만', async () => {
|
||||
it('시각 · 방식(앱 비밀번호 이름 · ARA SYSTEM 앱) · IP · 기기(앱은 운영체제, 메일 프로그램은 —) · 결과 — 실패는 빨간 굵은 글자만', async () => {
|
||||
const wrapper = await mountPage()
|
||||
const nowDate = new Date(NOW)
|
||||
const time = (index: number) => formatRelativeDayTime(EVENTS[index]?.occurredAt ?? '', nowDate)
|
||||
|
||||
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일과 메일 프로그램')
|
||||
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일, ARA SYSTEM 앱, 메일 프로그램')
|
||||
expect(wrapper.findAll('thead').at(1)?.findAll('th').map((cell) => cell.text())).toEqual([
|
||||
'시각',
|
||||
'방식',
|
||||
'IP',
|
||||
'기기',
|
||||
'결과',
|
||||
])
|
||||
expect(eventRows(wrapper)).toEqual([
|
||||
[time(0), '웹메일', '211.186.95.252', 'Chrome · Windows', '2단계 인증으로 성공'],
|
||||
[time(1), '웹메일', '211.186.95.252', 'Chrome · Windows', '코드가 틀려 실패'],
|
||||
[time(2), '메일 프로그램 · IMAP앱 비밀번호 ‘회사 PC 아웃룩’', '211.186.95.252', '—알 수 없음', '성공'],
|
||||
[time(3), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '앱 비밀번호가 필요해 실패'],
|
||||
[time(4), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '비밀번호가 틀려 실패'],
|
||||
[time(3), 'ARA SYSTEM 앱', '58.29.11.4', 'Android', '2단계 인증으로 성공'],
|
||||
[time(4), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '앱 비밀번호가 필요해 실패'],
|
||||
[time(5), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '비밀번호가 틀려 실패'],
|
||||
])
|
||||
expect(eventRows(wrapper)[0]?.[0]).toBe('오늘 오전 9:30')
|
||||
expect(eventRows(wrapper)[3]?.[0]).toBe('오늘 오전 8:47')
|
||||
expect(wrapper.findAll('[data-event-result]').map((cell) => cell.classes().includes('events__result--failure'))).toEqual([
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
])
|
||||
expect(wrapper.find('[data-events-more]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('운영체제를 모르는 앱 줄의 기기는 —', async () => {
|
||||
const unknownOs = loginEvent('app2', {
|
||||
method: 'app',
|
||||
device: { browser: 'ARA SYSTEM 앱', os: null },
|
||||
isSuccess: false,
|
||||
failureReason: 'wrongPassword',
|
||||
})
|
||||
const wrapper = await mountPage({ events: () => ok(pageOf([unknownOs])) })
|
||||
|
||||
expect(eventRows(wrapper)[0]?.slice(1)).toEqual(['ARA SYSTEM 앱', '211.186.95.252', '—알 수 없음', '비밀번호가 틀려 실패'])
|
||||
})
|
||||
|
||||
it('[실패만] — isSuccess=false 로 처음부터 받는다 · "최근 90일 · 실패 4번"(서버의 total) · 전환은 aria-pressed', async () => {
|
||||
const failures = EVENTS.filter((event) => !event.isSuccess)
|
||||
const wrapper = await mountPage({
|
||||
@@ -435,14 +541,14 @@ describe('AccountLoginHistoryPage', () => {
|
||||
await flushPromises()
|
||||
expect(gates).toHaveLength(2)
|
||||
expect(wrapper.get('.events__table-box').attributes('aria-busy')).toBe('true')
|
||||
expect(wrapper.findAll('[data-login-event]')).toHaveLength(5)
|
||||
expect(wrapper.findAll('[data-login-event]')).toHaveLength(6)
|
||||
|
||||
// 늦게 온 「실패만」 응답은 버린다
|
||||
gates[1]?.(ok(pageOf([EVENTS[0] as LoginEvent])))
|
||||
gates[0]?.(ok(pageOf([EVENTS[1] as LoginEvent], null, 1)))
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('[data-login-event]').map((row) => row.attributes('data-login-event'))).toEqual(['e1'])
|
||||
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일과 메일 프로그램')
|
||||
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일, ARA SYSTEM 앱, 메일 프로그램')
|
||||
expect(wrapper.get('.events__table-box').attributes('aria-busy')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -469,7 +575,7 @@ describe('AccountLoginHistoryPage', () => {
|
||||
release(ok(pageOf(EVENTS)))
|
||||
await flushPromises()
|
||||
expect(again.get('[data-events-filter="all"]').attributes('aria-pressed')).toBe('true')
|
||||
expect(again.findAll('[data-login-event]')).toHaveLength(5)
|
||||
expect(again.findAll('[data-login-event]')).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('기록을 받지 못하면 그 묶음만 사전 문구 + [다시 시도]', async () => {
|
||||
@@ -477,12 +583,12 @@ describe('AccountLoginHistoryPage', () => {
|
||||
const wrapper = await mountPage({ events: () => reply })
|
||||
|
||||
expect(wrapper.get('[data-events-error]').text()).toContain(ERROR_CODE_MESSAGES.SYS_001)
|
||||
expect(sessionRows(wrapper)).toHaveLength(3)
|
||||
expect(sessionRows(wrapper)).toHaveLength(4)
|
||||
|
||||
reply = ok(pageOf(EVENTS))
|
||||
await wrapper.get('[data-events-error] .mail-load-error__action').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('[data-login-event]')).toHaveLength(5)
|
||||
expect(wrapper.findAll('[data-login-event]')).toHaveLength(6)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 설정 › 계정 › 로그인 기록 (디자인 원본 SettingsLoginHistory · README "12단계 › 설정 › 계정 › 로그인 기록" · 계약 5절 "로그인된 브라우저" · "로그인 기록")
|
||||
// — /settings/account/login-history · 탭 제목 "로그인 기록"
|
||||
// 머리 "로그인 기록" + 설명(「비밀번호를 바꾸고」 · 「2단계 인증」 링크) → 로그인된 브라우저(LoginSessionList) → 최근 로그인(LoginEventTable)
|
||||
// 머리 "로그인 기록" + 설명(「비밀번호를 바꾸고」 · 「2단계 인증」 링크) → 로그인된 기기(LoginSessionList — 웹메일 · ARA SYSTEM 앱 세션) → 최근 로그인(LoginEventTable)
|
||||
// - 들어올 때마다 둘 다 새로 받는다(login-history.store). 묶음마다 받는 동안은 자리만, 못 받으면 지금의 목록 오류와 같은 모양(사전 문구 + [다시 시도])
|
||||
// - 끊긴 브라우저는 다음 요청에서 바로 로그인 화면으로 간다(서버가 끊은 세션 id 를 막는다 — 계약 5절)
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -81,10 +81,11 @@ describe('AccountPasswordPage', () => {
|
||||
expect(wrapper.get('#password-next-hint').text()).toBe(
|
||||
'8자 이상이고 지금 비밀번호와 달라야 합니다. 영문, 숫자, 기호를 섞으면 더 안전합니다.',
|
||||
)
|
||||
// 모든 직원 — 다른 곳 로그인이 끊기고 앱 비밀번호도 모두 지워진다 (12단계)
|
||||
// 모든 직원 — 다른 곳 로그인(웹메일 · ARA SYSTEM 앱)이 끊기고 앱 비밀번호도 모두 지워진다 (12단계 · 앱 1단계)
|
||||
expect(wrapper.get('[data-password-note]').text()).toContain(
|
||||
'바꾸면 이 브라우저를 빼고 다른 기기와 브라우저의 웹메일에서 로그아웃됩니다.',
|
||||
'바꾸면 이 브라우저를 빼고 다른 기기와 브라우저에서 모두 로그아웃됩니다.',
|
||||
)
|
||||
expect(wrapper.get('[data-password-note]').text()).not.toContain('웹메일에서 로그아웃')
|
||||
expect(wrapper.get('[data-password-note]').text()).toContain(
|
||||
'앱 비밀번호도 모두 지워지니, 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요.',
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 설정 › 계정 › 비밀번호 (디자인 원본 SettingsAccount · README "4단계 › 비밀번호" · 12단계 SettingsTwoFactor "비밀번호 — 2단계 인증을 켠 직원")
|
||||
// — /settings/account/password · 설정의 첫 화면
|
||||
// 바꾸는 폼(PasswordChangeForm) 아래에 안내 상자(바탕 #F3F8F8): 다른 기기의 웹메일 로그인이 끊기는 것 · 앱 비밀번호도 모두 지워지는 것(12단계 — 모든 직원)
|
||||
// 바꾸는 폼(PasswordChangeForm) 아래에 안내 상자(바탕 #F3F8F8): 다른 기기와 브라우저의 로그인(웹메일 · ARA SYSTEM 앱)이 끊기는 것 · 앱 비밀번호도 모두 지워지는 것(12단계 — 모든 직원)
|
||||
// 2단계 인증 상태는 들어올 때마다 새로 받는다(two-factor.store) — 켠 직원이면 머리 설명이 바뀌고 양식 맨 아래에 「인증 앱 코드」 칸이 생긴다.
|
||||
// 못 받아도 그대로 쓸 수 있다 — 켰는데 코드 없이 보내면 서버가 AUTH_007 로 알려 와 양식이 상태를 다시 받고 칸을 보인다
|
||||
import { computed } from 'vue'
|
||||
|
||||
Reference in New Issue
Block a user