@blastlabs/utils 1.12.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/components/auth/AuthGuard.d.ts +95 -0
- package/dist/components/auth/AuthGuard.d.ts.map +1 -0
- package/dist/components/auth/AuthGuard.js +103 -0
- package/dist/components/auth/index.d.ts +5 -0
- package/dist/components/auth/index.d.ts.map +1 -0
- package/dist/components/auth/index.js +4 -0
- package/dist/components/dev/ApiLogger.d.ts +1 -1
- package/dist/components/dev/ApiLogger.js +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/hooks/auth/__tests__/useAuth.test.d.ts +2 -0
- package/dist/hooks/auth/__tests__/useAuth.test.d.ts.map +1 -0
- package/dist/hooks/auth/__tests__/useAuth.test.js +139 -0
- package/dist/hooks/auth/index.d.ts +5 -0
- package/dist/hooks/auth/index.d.ts.map +1 -0
- package/dist/hooks/auth/index.js +4 -0
- package/dist/hooks/auth/useAuth.d.ts +275 -0
- package/dist/hooks/auth/useAuth.d.ts.map +1 -0
- package/dist/hooks/auth/useAuth.js +384 -0
- package/dist/hooks/form/useCRUDForm.d.ts +7 -28
- package/dist/hooks/form/useCRUDForm.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/index.js +1 -0
- package/dist/hooks/ui/useTabs.d.ts +33 -0
- package/dist/hooks/ui/useTabs.d.ts.map +1 -0
- package/dist/hooks/ui/useTabs.js +117 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blastlabs/utils
|
|
2
|
+
|
|
3
|
+
> **⚠️ 주의: 이 라이브러리는 사내 개발용이며, 현재 테스트 단계입니다.**
|
|
2
4
|
|
|
3
5
|
React 프로젝트에서 자주 사용하는 유틸리티 훅(Hooks)과 개발용 컴포넌트 모음입니다.
|
|
4
6
|
|
|
5
7
|
## 설치
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install
|
|
10
|
+
npm install @blastlabs/utils
|
|
9
11
|
# or
|
|
10
|
-
yarn add
|
|
12
|
+
yarn add @blastlabs/utils
|
|
11
13
|
# or
|
|
12
|
-
pnpm add
|
|
14
|
+
pnpm add @blastlabs/utils
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## 기능
|
|
@@ -19,7 +21,7 @@ pnpm add goodchuck-utils
|
|
|
19
21
|
프로젝트에서 자주 사용하는 커스텀 훅들을 제공합니다.
|
|
20
22
|
|
|
21
23
|
```typescript
|
|
22
|
-
import { useDebounce, useToggle, useCopyToClipboard } from '
|
|
24
|
+
import { useDebounce, useToggle, useCopyToClipboard } from '@blastlabs/utils/hooks';
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
#### 사용 가능한 Hooks
|
|
@@ -38,7 +40,7 @@ import { useDebounce, useToggle, useCopyToClipboard } from 'goodchuck-utils/hook
|
|
|
38
40
|
개발 환경에서만 사용하는 유틸리티 컴포넌트들입니다.
|
|
39
41
|
|
|
40
42
|
```typescript
|
|
41
|
-
import { DevPanel, FormDevTools, ApiLogger } from '
|
|
43
|
+
import { DevPanel, FormDevTools, ApiLogger } from '@blastlabs/utils/components/dev';
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
#### IdSelector
|
|
@@ -46,7 +48,7 @@ import { DevPanel, FormDevTools, ApiLogger } from 'goodchuck-utils/components/de
|
|
|
46
48
|
개발 환경에서 여러 계정으로 빠르게 로그인할 수 있는 컴포넌트입니다.
|
|
47
49
|
|
|
48
50
|
```tsx
|
|
49
|
-
import { IdSelector } from '
|
|
51
|
+
import { IdSelector } from '@blastlabs/utils/components/dev';
|
|
50
52
|
|
|
51
53
|
const devAccounts = [
|
|
52
54
|
{ id: 'admin', pw: 'admin123', memo: '관리자' },
|
|
@@ -75,7 +77,7 @@ react-hook-form의 상태를 실시간으로 시각화하는 개발용 컴포넌
|
|
|
75
77
|
|
|
76
78
|
```tsx
|
|
77
79
|
import { useForm } from 'react-hook-form';
|
|
78
|
-
import { FormDevTools } from '
|
|
80
|
+
import { FormDevTools } from '@blastlabs/utils/components/dev';
|
|
79
81
|
|
|
80
82
|
function MyForm() {
|
|
81
83
|
const form = useForm({
|
|
@@ -112,7 +114,7 @@ API 요청/응답을 로깅하고 모니터링하는 컴포넌트입니다.
|
|
|
112
114
|
|
|
113
115
|
```tsx
|
|
114
116
|
import axios from 'axios';
|
|
115
|
-
import { ApiLogger, addApiLog } from '
|
|
117
|
+
import { ApiLogger, addApiLog } from '@blastlabs/utils/components/dev';
|
|
116
118
|
|
|
117
119
|
// Axios interceptor 설정
|
|
118
120
|
axios.interceptors.request.use(
|
|
@@ -178,7 +180,7 @@ function App() {
|
|
|
178
180
|
여러 개발 도구를 하나의 패널에서 관리할 수 있는 통합 컴포넌트입니다.
|
|
179
181
|
|
|
180
182
|
```tsx
|
|
181
|
-
import { DevPanel } from '
|
|
183
|
+
import { DevPanel } from '@blastlabs/utils/components/dev';
|
|
182
184
|
|
|
183
185
|
function App() {
|
|
184
186
|
return (
|
|
@@ -200,7 +202,7 @@ function App() {
|
|
|
200
202
|
페이지의 모든 z-index 값을 시각화하고 디버깅하는 도구입니다.
|
|
201
203
|
|
|
202
204
|
```tsx
|
|
203
|
-
import { ZIndexDebugger } from '
|
|
205
|
+
import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
|
|
204
206
|
|
|
205
207
|
function App() {
|
|
206
208
|
return (
|
|
@@ -222,7 +224,7 @@ function App() {
|
|
|
222
224
|
현재 윈도우 크기를 화면에 표시하는 간단한 컴포넌트입니다.
|
|
223
225
|
|
|
224
226
|
```tsx
|
|
225
|
-
import { WindowSizeDisplay } from '
|
|
227
|
+
import { WindowSizeDisplay } from '@blastlabs/utils/components/dev';
|
|
226
228
|
|
|
227
229
|
function App() {
|
|
228
230
|
return (
|
|
@@ -250,7 +252,7 @@ function App() {
|
|
|
250
252
|
모든 컴포넌트와 훅은 TypeScript로 작성되었으며, 타입 정의가 포함되어 있습니다.
|
|
251
253
|
|
|
252
254
|
```typescript
|
|
253
|
-
import type { FormDevToolsProps, ApiLogEntry } from '
|
|
255
|
+
import type { FormDevToolsProps, ApiLogEntry } from '@blastlabs/utils/components/dev';
|
|
254
256
|
```
|
|
255
257
|
|
|
256
258
|
## 주의사항
|
|
@@ -258,6 +260,11 @@ import type { FormDevToolsProps, ApiLogEntry } from 'goodchuck-utils/components/
|
|
|
258
260
|
- **개발용 컴포넌트(`components/dev`)는 프로덕션 환경에서 제외하는 것을 권장합니다.**
|
|
259
261
|
- 개발 환경 구분을 위해 `import.meta.env.DEV` (Vite) 또는 `process.env.NODE_ENV === 'development'` (CRA)를 사용하세요.
|
|
260
262
|
|
|
263
|
+
## 문의
|
|
264
|
+
|
|
265
|
+
- goodchuck852@gmail.com
|
|
266
|
+
- taehyunyang@blast-team.com
|
|
267
|
+
|
|
261
268
|
## 라이선스
|
|
262
269
|
|
|
263
270
|
ISC
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { UseAuthOptions } from '../../hooks/auth';
|
|
4
|
+
type AuthGuardProps = {
|
|
5
|
+
/** 인증된 경우 렌더링할 컴포넌트 */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** 로딩 중 표시할 컴포넌트 */
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
/** 인증되지 않은 경우 호출되는 콜백 (리다이렉트 로직 구현) */
|
|
10
|
+
onUnauthenticated?: () => void;
|
|
11
|
+
/** 로딩 완료 후 인증되지 않은 경우 렌더링할 컴포넌트 (onUnauthenticated가 없을 때) */
|
|
12
|
+
unauthenticatedFallback?: ReactNode;
|
|
13
|
+
/** useAuth 훅 옵션 (선택사항) */
|
|
14
|
+
authOptions?: UseAuthOptions;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 인증 가드 컴포넌트
|
|
18
|
+
* 인증이 필요한 페이지를 보호하는 컴포넌트
|
|
19
|
+
*
|
|
20
|
+
* 내부적으로 useAuth 훅을 호출하여 localStorage/sessionStorage에서 인증 상태를 확인합니다.
|
|
21
|
+
* 각 AuthGuard는 독립적으로 동작하며, storage를 통해 자동으로 동기화됩니다.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // 기본 사용 (localStorage 자동 로드)
|
|
26
|
+
* import { AuthGuard } from '@blastlabs/utils';
|
|
27
|
+
* import { useRouter } from 'next/navigation';
|
|
28
|
+
*
|
|
29
|
+
* function ProtectedPage() {
|
|
30
|
+
* const router = useRouter();
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <AuthGuard
|
|
34
|
+
* authOptions={{ autoLoadUser: true }}
|
|
35
|
+
* onUnauthenticated={() => router.push('/login')}
|
|
36
|
+
* fallback={<LoadingSpinner />}
|
|
37
|
+
* >
|
|
38
|
+
* <YourProtectedContent />
|
|
39
|
+
* </AuthGuard>
|
|
40
|
+
* );
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* // React Router에서 사용
|
|
47
|
+
* import { useNavigate } from 'react-router-dom';
|
|
48
|
+
*
|
|
49
|
+
* function ProtectedPage() {
|
|
50
|
+
* const navigate = useNavigate();
|
|
51
|
+
*
|
|
52
|
+
* return (
|
|
53
|
+
* <AuthGuard
|
|
54
|
+
* authOptions={{ autoLoadUser: true }}
|
|
55
|
+
* onUnauthenticated={() => navigate('/login')}
|
|
56
|
+
* >
|
|
57
|
+
* <YourProtectedContent />
|
|
58
|
+
* </AuthGuard>
|
|
59
|
+
* );
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // 리다이렉트 없이 대체 UI 표시
|
|
66
|
+
* <AuthGuard
|
|
67
|
+
* authOptions={{ autoLoadUser: true }}
|
|
68
|
+
* unauthenticatedFallback={
|
|
69
|
+
* <div>
|
|
70
|
+
* <h1>로그인이 필요합니다</h1>
|
|
71
|
+
* <Link to="/login">로그인 페이지로 이동</Link>
|
|
72
|
+
* </div>
|
|
73
|
+
* }
|
|
74
|
+
* >
|
|
75
|
+
* <YourProtectedContent />
|
|
76
|
+
* </AuthGuard>
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* // 여러 페이지에서 사용해도 storage로 자동 동기화
|
|
82
|
+
* // Page1.tsx
|
|
83
|
+
* <AuthGuard authOptions={{ autoLoadUser: true }} onUnauthenticated={...}>
|
|
84
|
+
* <Page1Content />
|
|
85
|
+
* </AuthGuard>
|
|
86
|
+
*
|
|
87
|
+
* // Page2.tsx - 동일한 인증 상태 공유 (localStorage 사용)
|
|
88
|
+
* <AuthGuard authOptions={{ autoLoadUser: true }} onUnauthenticated={...}>
|
|
89
|
+
* <Page2Content />
|
|
90
|
+
* </AuthGuard>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function AuthGuard({ children, fallback, onUnauthenticated, unauthenticatedFallback, authOptions, }: AuthGuardProps): React.JSX.Element | null;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=AuthGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthGuard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAa,MAAM,OAAO,CAAC;AAC7C,OAAO,EAA0B,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAE1E,KAAK,cAAc,GAAG;IACpB,uBAAuB;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,SAAS,CAAC;IACpC,0BAA0B;IAC1B,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,QAAQ,EACR,iBAAiB,EACjB,uBAAuB,EACvB,WAAW,GACZ,EAAE,cAAc,4BA0BhB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useAuth as useAuthHook } from '../../hooks/auth';
|
|
4
|
+
/**
|
|
5
|
+
* 인증 가드 컴포넌트
|
|
6
|
+
* 인증이 필요한 페이지를 보호하는 컴포넌트
|
|
7
|
+
*
|
|
8
|
+
* 내부적으로 useAuth 훅을 호출하여 localStorage/sessionStorage에서 인증 상태를 확인합니다.
|
|
9
|
+
* 각 AuthGuard는 독립적으로 동작하며, storage를 통해 자동으로 동기화됩니다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // 기본 사용 (localStorage 자동 로드)
|
|
14
|
+
* import { AuthGuard } from '@blastlabs/utils';
|
|
15
|
+
* import { useRouter } from 'next/navigation';
|
|
16
|
+
*
|
|
17
|
+
* function ProtectedPage() {
|
|
18
|
+
* const router = useRouter();
|
|
19
|
+
*
|
|
20
|
+
* return (
|
|
21
|
+
* <AuthGuard
|
|
22
|
+
* authOptions={{ autoLoadUser: true }}
|
|
23
|
+
* onUnauthenticated={() => router.push('/login')}
|
|
24
|
+
* fallback={<LoadingSpinner />}
|
|
25
|
+
* >
|
|
26
|
+
* <YourProtectedContent />
|
|
27
|
+
* </AuthGuard>
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* // React Router에서 사용
|
|
35
|
+
* import { useNavigate } from 'react-router-dom';
|
|
36
|
+
*
|
|
37
|
+
* function ProtectedPage() {
|
|
38
|
+
* const navigate = useNavigate();
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <AuthGuard
|
|
42
|
+
* authOptions={{ autoLoadUser: true }}
|
|
43
|
+
* onUnauthenticated={() => navigate('/login')}
|
|
44
|
+
* >
|
|
45
|
+
* <YourProtectedContent />
|
|
46
|
+
* </AuthGuard>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* // 리다이렉트 없이 대체 UI 표시
|
|
54
|
+
* <AuthGuard
|
|
55
|
+
* authOptions={{ autoLoadUser: true }}
|
|
56
|
+
* unauthenticatedFallback={
|
|
57
|
+
* <div>
|
|
58
|
+
* <h1>로그인이 필요합니다</h1>
|
|
59
|
+
* <Link to="/login">로그인 페이지로 이동</Link>
|
|
60
|
+
* </div>
|
|
61
|
+
* }
|
|
62
|
+
* >
|
|
63
|
+
* <YourProtectedContent />
|
|
64
|
+
* </AuthGuard>
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* // 여러 페이지에서 사용해도 storage로 자동 동기화
|
|
70
|
+
* // Page1.tsx
|
|
71
|
+
* <AuthGuard authOptions={{ autoLoadUser: true }} onUnauthenticated={...}>
|
|
72
|
+
* <Page1Content />
|
|
73
|
+
* </AuthGuard>
|
|
74
|
+
*
|
|
75
|
+
* // Page2.tsx - 동일한 인증 상태 공유 (localStorage 사용)
|
|
76
|
+
* <AuthGuard authOptions={{ autoLoadUser: true }} onUnauthenticated={...}>
|
|
77
|
+
* <Page2Content />
|
|
78
|
+
* </AuthGuard>
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function AuthGuard({ children, fallback, onUnauthenticated, unauthenticatedFallback, authOptions, }) {
|
|
82
|
+
const { isAuthenticated, isLoading } = useAuthHook(authOptions);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!isLoading && !isAuthenticated && onUnauthenticated) {
|
|
85
|
+
onUnauthenticated();
|
|
86
|
+
}
|
|
87
|
+
}, [isAuthenticated, isLoading, onUnauthenticated]);
|
|
88
|
+
// 로딩 중이면 fallback 표시
|
|
89
|
+
if (isLoading) {
|
|
90
|
+
return React.createElement(React.Fragment, null, fallback || null);
|
|
91
|
+
}
|
|
92
|
+
// 인증되지 않았으면
|
|
93
|
+
if (!isAuthenticated) {
|
|
94
|
+
// onUnauthenticated 콜백이 있으면 리다이렉트 중이므로 null
|
|
95
|
+
if (onUnauthenticated) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// 콜백이 없으면 unauthenticatedFallback 표시
|
|
99
|
+
return React.createElement(React.Fragment, null, unauthenticatedFallback || null);
|
|
100
|
+
}
|
|
101
|
+
// 인증되었으면 자식 컴포넌트 렌더링
|
|
102
|
+
return React.createElement(React.Fragment, null, children);
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/auth/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,aAAa,CAAC"}
|
|
@@ -92,7 +92,7 @@ export declare function clearApiLogs(): void;
|
|
|
92
92
|
* @example
|
|
93
93
|
* ```tsx
|
|
94
94
|
* // fetch wrapper 사용
|
|
95
|
-
* import { addApiLog } from '
|
|
95
|
+
* import { addApiLog } from '@blastlabs/utils/components/dev';
|
|
96
96
|
*
|
|
97
97
|
* const originalFetch = window.fetch;
|
|
98
98
|
* window.fetch = async (...args) => {
|
|
@@ -89,7 +89,7 @@ export function clearApiLogs() {
|
|
|
89
89
|
* @example
|
|
90
90
|
* ```tsx
|
|
91
91
|
* // fetch wrapper 사용
|
|
92
|
-
* import { addApiLog } from '
|
|
92
|
+
* import { addApiLog } from '@blastlabs/utils/components/dev';
|
|
93
93
|
*
|
|
94
94
|
* const originalFetch = window.fetch;
|
|
95
95
|
* window.fetch = async (...args) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/auth/__tests__/useAuth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
3
|
+
import { useAuth } from '../useAuth';
|
|
4
|
+
describe('useAuth', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
sessionStorage.clear();
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
localStorage.clear();
|
|
12
|
+
sessionStorage.clear();
|
|
13
|
+
});
|
|
14
|
+
describe('Initialization', () => {
|
|
15
|
+
it('should initialize with no user', () => {
|
|
16
|
+
const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
|
|
17
|
+
expect(result.current.user).toBeNull();
|
|
18
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
19
|
+
expect(result.current.isLoading).toBe(false);
|
|
20
|
+
expect(result.current.token).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it('should load user from localStorage on mount', async () => {
|
|
23
|
+
const mockUser = { id: '1', name: 'Test User' };
|
|
24
|
+
localStorage.setItem('auth_token', 'test-token');
|
|
25
|
+
localStorage.setItem('auth_user', JSON.stringify(mockUser));
|
|
26
|
+
const { result } = renderHook(() => useAuth({ autoLoadUser: true }));
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
expect(result.current.user).toEqual(mockUser);
|
|
29
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('Login', () => {
|
|
34
|
+
it('should login successfully', async () => {
|
|
35
|
+
const mockUser = { id: '1', name: 'Test User' };
|
|
36
|
+
const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'test-token' });
|
|
37
|
+
const onLoginSuccess = vi.fn();
|
|
38
|
+
const { result } = renderHook(() => useAuth({ loginFn, onLoginSuccess, autoLoadUser: false }));
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
41
|
+
});
|
|
42
|
+
expect(result.current.user).toEqual(mockUser);
|
|
43
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
44
|
+
expect(result.current.token).toBe('test-token');
|
|
45
|
+
expect(onLoginSuccess).toHaveBeenCalledWith(mockUser);
|
|
46
|
+
expect(localStorage.getItem('auth_token')).toBe('test-token');
|
|
47
|
+
});
|
|
48
|
+
it('should handle login error', async () => {
|
|
49
|
+
const loginError = new Error('Invalid credentials');
|
|
50
|
+
const loginFn = vi.fn().mockRejectedValue(loginError);
|
|
51
|
+
const onError = vi.fn();
|
|
52
|
+
const { result } = renderHook(() => useAuth({ loginFn, onError, autoLoadUser: false }));
|
|
53
|
+
let thrownError;
|
|
54
|
+
try {
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await result.current.login({ email: 'test@example.com', password: 'wrong' });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
thrownError = error;
|
|
61
|
+
}
|
|
62
|
+
expect(thrownError).toEqual(loginError);
|
|
63
|
+
expect(result.current.user).toBeNull();
|
|
64
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
65
|
+
expect(onError).toHaveBeenCalledWith(loginError, 'login');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('Logout', () => {
|
|
69
|
+
it('should logout successfully', async () => {
|
|
70
|
+
const mockUser = { id: '1', name: 'Test User' };
|
|
71
|
+
const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'test-token' });
|
|
72
|
+
const logoutFn = vi.fn().mockResolvedValue(undefined);
|
|
73
|
+
const onLogoutSuccess = vi.fn();
|
|
74
|
+
const { result } = renderHook(() => useAuth({ loginFn, logoutFn, onLogoutSuccess, autoLoadUser: false }));
|
|
75
|
+
await act(async () => {
|
|
76
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
77
|
+
});
|
|
78
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
79
|
+
await act(async () => {
|
|
80
|
+
await result.current.logout();
|
|
81
|
+
});
|
|
82
|
+
expect(result.current.user).toBeNull();
|
|
83
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
84
|
+
expect(result.current.token).toBeNull();
|
|
85
|
+
expect(onLogoutSuccess).toHaveBeenCalled();
|
|
86
|
+
expect(localStorage.getItem('auth_token')).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('Register', () => {
|
|
90
|
+
it('should register successfully', async () => {
|
|
91
|
+
const mockUser = { id: '1', name: 'New User' };
|
|
92
|
+
const registerFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'new-token' });
|
|
93
|
+
const onRegisterSuccess = vi.fn();
|
|
94
|
+
const { result } = renderHook(() => useAuth({ registerFn, onRegisterSuccess, autoLoadUser: false }));
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await result.current.register({ email: 'new@example.com', password: 'password', name: 'New User' });
|
|
97
|
+
});
|
|
98
|
+
expect(result.current.user).toEqual(mockUser);
|
|
99
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
100
|
+
expect(result.current.token).toBe('new-token');
|
|
101
|
+
expect(onRegisterSuccess).toHaveBeenCalledWith(mockUser);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('Token Management', () => {
|
|
105
|
+
it('should manually set token', async () => {
|
|
106
|
+
const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(result.current.isLoading).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
act(() => {
|
|
111
|
+
result.current.setToken('manual-token');
|
|
112
|
+
});
|
|
113
|
+
expect(result.current.token).toBe('manual-token');
|
|
114
|
+
expect(localStorage.getItem('auth_token')).toBe('manual-token');
|
|
115
|
+
});
|
|
116
|
+
it('should manually set user', async () => {
|
|
117
|
+
const mockUser = { id: '1', name: 'Manual User' };
|
|
118
|
+
const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(result.current.isLoading).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
act(() => {
|
|
123
|
+
result.current.setUser(mockUser);
|
|
124
|
+
});
|
|
125
|
+
expect(result.current.user).toEqual(mockUser);
|
|
126
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
it('should use sessionStorage when useSessionStorage is true', async () => {
|
|
129
|
+
const mockUser = { id: '1', name: 'Test User' };
|
|
130
|
+
const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'session-token' });
|
|
131
|
+
const { result } = renderHook(() => useAuth({ loginFn, useSessionStorage: true, autoLoadUser: false }));
|
|
132
|
+
await act(async () => {
|
|
133
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
134
|
+
});
|
|
135
|
+
expect(sessionStorage.getItem('auth_token')).toBe('session-token');
|
|
136
|
+
expect(localStorage.getItem('auth_token')).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/auth/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,WAAW,CAAC"}
|