- 하단 내비게이션(뒤로/앞으로/홈/새로고침/설정), 사이드바 홈 제거 - 설정 화면: 계정정보·앱 버전(package.json)·App Data 삭제 - 가입정보 변경: 비밀번호 재인증 → 이름/이메일 수정 - 로그인 후 대시보드(/) 이동, 로그인 전 햄버거·네이버 로그인 버튼 숨김 - Vitest 도입(32): authApi/accountApi 와이어링, auth/ui 스토어, 로그인 플로우 - CI(.gitea): npm test 게이트 추가 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -84,13 +84,19 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
await clear()
|
||||
}
|
||||
|
||||
// 가입정보 변경 등으로 바뀐 사용자 필드를 로컬 세션에 병합 (헤더 이름 등 즉시 반영)
|
||||
async function applyUser(partial) {
|
||||
user.value = { ...(user.value || {}), ...partial }
|
||||
await persist()
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
token.value = ''
|
||||
user.value = null
|
||||
await persist()
|
||||
}
|
||||
|
||||
return { token, user, ready, isAuthenticated, restore, login, signup, fetchMe, logout, clear }
|
||||
return { token, user, ready, isAuthenticated, restore, login, signup, fetchMe, applyUser, logout, clear }
|
||||
})
|
||||
|
||||
function safeParse(value) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
vi.mock('@/api/authApi', () => ({
|
||||
authApi: {
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(() => Promise.resolve()),
|
||||
me: vi.fn(),
|
||||
signup: vi.fn(),
|
||||
signupEnabled: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// 네이티브 저장소 — 테스트에선 no-op (localStorage 미러만 검증)
|
||||
vi.mock('@capacitor/preferences', () => ({
|
||||
Preferences: {
|
||||
set: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve({ value: null })),
|
||||
remove: vi.fn(() => Promise.resolve()),
|
||||
clear: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
}))
|
||||
|
||||
import { authApi } from '@/api/authApi'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
describe('auth 스토어', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('login: 토큰/사용자 저장 + localStorage 미러 + isAuthenticated', async () => {
|
||||
authApi.login.mockResolvedValue({
|
||||
token: 'tok123',
|
||||
member: { id: 1, loginId: 'u1', name: '홍길동', email: 'a@b.com' },
|
||||
})
|
||||
const store = useAuthStore()
|
||||
await store.login('u1', 'pw', true)
|
||||
|
||||
expect(authApi.login).toHaveBeenCalledWith({ loginId: 'u1', password: 'pw', rememberMe: true })
|
||||
expect(store.token).toBe('tok123')
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
expect(store.user.email).toBe('a@b.com')
|
||||
expect(localStorage.getItem('token')).toBe('tok123')
|
||||
expect(JSON.parse(localStorage.getItem('auth_user')).name).toBe('홍길동')
|
||||
})
|
||||
|
||||
it('logout: 상태/스토리지 정리', async () => {
|
||||
authApi.login.mockResolvedValue({ token: 't', member: { id: 1, loginId: 'u1' } })
|
||||
const store = useAuthStore()
|
||||
await store.login('u1', 'pw', false)
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
|
||||
await store.logout()
|
||||
expect(authApi.logout).toHaveBeenCalled()
|
||||
expect(store.token).toBe('')
|
||||
expect(store.user).toBeNull()
|
||||
expect(store.isAuthenticated).toBe(false)
|
||||
expect(localStorage.getItem('token')).toBeNull()
|
||||
expect(localStorage.getItem('auth_user')).toBeNull()
|
||||
})
|
||||
|
||||
it('logout: 서버 401 이어도 클라이언트는 정리', async () => {
|
||||
authApi.login.mockResolvedValue({ token: 't', member: { id: 1 } })
|
||||
authApi.logout.mockRejectedValueOnce(new Error('401'))
|
||||
const store = useAuthStore()
|
||||
await store.login('u1', 'pw', false)
|
||||
|
||||
await store.logout()
|
||||
expect(store.token).toBe('')
|
||||
expect(store.isAuthenticated).toBe(false)
|
||||
})
|
||||
|
||||
it('restore: localStorage 세션 복원 + ready', async () => {
|
||||
localStorage.setItem('token', 't2')
|
||||
localStorage.setItem('auth_user', JSON.stringify({ id: 2, name: '복원유저' }))
|
||||
const store = useAuthStore()
|
||||
await store.restore()
|
||||
|
||||
expect(store.token).toBe('t2')
|
||||
expect(store.user.name).toBe('복원유저')
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
expect(store.ready).toBe(true)
|
||||
})
|
||||
|
||||
it('applyUser: 변경 필드 병합 + 미러 갱신 (가입정보 변경 반영)', async () => {
|
||||
authApi.login.mockResolvedValue({
|
||||
token: 't',
|
||||
member: { id: 1, loginId: 'u1', name: '원래이름', email: 'old@b.com' },
|
||||
})
|
||||
const store = useAuthStore()
|
||||
await store.login('u1', 'pw', false)
|
||||
|
||||
await store.applyUser({ name: '새이름', email: 'new@b.com' })
|
||||
expect(store.user.name).toBe('새이름')
|
||||
expect(store.user.loginId).toBe('u1') // 기존 필드 보존
|
||||
expect(JSON.parse(localStorage.getItem('auth_user')).email).toBe('new@b.com')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
vi.mock('@/api/authApi', () => ({
|
||||
authApi: {
|
||||
signupEnabled: vi.fn(() => Promise.resolve({ enabled: true })),
|
||||
},
|
||||
}))
|
||||
|
||||
import { authApi } from '@/api/authApi'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
|
||||
describe('ui 스토어', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
it('openLogin: 로그인 열고 회원가입 닫고 redirect 보존', () => {
|
||||
const ui = useUiStore()
|
||||
ui.signupOpen = true
|
||||
ui.openLogin('/account/entries')
|
||||
expect(ui.loginOpen).toBe(true)
|
||||
expect(ui.signupOpen).toBe(false)
|
||||
expect(ui.redirectPath).toBe('/account/entries')
|
||||
})
|
||||
|
||||
it('closeLogin: 닫고 redirect 초기화', () => {
|
||||
const ui = useUiStore()
|
||||
ui.openLogin('/x')
|
||||
ui.closeLogin()
|
||||
expect(ui.loginOpen).toBe(false)
|
||||
expect(ui.redirectPath).toBe('')
|
||||
})
|
||||
|
||||
it('openSignup: 회원가입 열고 로그인 닫고 허용여부 재조회', () => {
|
||||
const ui = useUiStore()
|
||||
ui.loginOpen = true
|
||||
ui.openSignup()
|
||||
expect(ui.signupOpen).toBe(true)
|
||||
expect(ui.loginOpen).toBe(false)
|
||||
expect(authApi.signupEnabled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('toggleSidebar / closeSidebar', () => {
|
||||
const ui = useUiStore()
|
||||
expect(ui.sidebarOpen).toBe(false)
|
||||
ui.toggleSidebar()
|
||||
expect(ui.sidebarOpen).toBe(true)
|
||||
ui.closeSidebar()
|
||||
expect(ui.sidebarOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('loadSignupEnabled: API 값 반영', async () => {
|
||||
authApi.signupEnabled.mockResolvedValueOnce({ enabled: false })
|
||||
const ui = useUiStore()
|
||||
await ui.loadSignupEnabled()
|
||||
expect(ui.signupEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it('loadSignupEnabled: 조회 실패 시 기본 허용(true)', async () => {
|
||||
authApi.signupEnabled.mockRejectedValueOnce(new Error('net'))
|
||||
const ui = useUiStore()
|
||||
await ui.loadSignupEnabled()
|
||||
expect(ui.signupEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user