fix: support develop branch deployment and fix React Query v5 compatibility
This commit is contained in:
@@ -2,7 +2,7 @@ name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, develop ]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker:27-cli
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* 认证相关 Hooks
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as authApi from '@/api/auth';
|
||||
@@ -12,34 +11,44 @@ export const useLogin = () => {
|
||||
const navigate = useNavigate();
|
||||
const setAuth = useAuthStore((state) => state.setAuth);
|
||||
|
||||
return useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.login,
|
||||
onSuccess: (data) => {
|
||||
// 先保存 token
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
|
||||
// 获取用户信息
|
||||
authApi.getCurrentUser().then((user) => {
|
||||
setAuth(user, data.access_token, data.refresh_token);
|
||||
navigate('/workspaces');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 手动处理成功后的逻辑
|
||||
const login = async (credentials: Parameters<typeof authApi.login>[0]) => {
|
||||
const data = await mutation.mutateAsync(credentials);
|
||||
// 先保存 token
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
|
||||
// 获取用户信息
|
||||
const user = await authApi.getCurrentUser();
|
||||
setAuth(user, data.access_token, data.refresh_token);
|
||||
navigate('/workspaces');
|
||||
return data;
|
||||
};
|
||||
|
||||
return { ...mutation, mutateAsync: login };
|
||||
};
|
||||
|
||||
// 注册 Hook
|
||||
export const useRegister = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.register,
|
||||
onSuccess: () => {
|
||||
navigate('/login', {
|
||||
state: { message: '注册成功!请查收验证邮件。' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const register = async (data: Parameters<typeof authApi.register>[0]) => {
|
||||
const result = await mutation.mutateAsync(data);
|
||||
navigate('/login', {
|
||||
state: { message: '注册成功!请查收验证邮件。' },
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return { ...mutation, mutateAsync: register };
|
||||
};
|
||||
|
||||
// 登出 Hook
|
||||
@@ -48,38 +57,32 @@ export const useLogout = () => {
|
||||
const clearAuth = useAuthStore((state) => state.clearAuth);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.logout,
|
||||
onSuccess: () => {
|
||||
clearAuth();
|
||||
queryClient.clear();
|
||||
navigate('/login');
|
||||
},
|
||||
onError: () => {
|
||||
// 即使登出失败也清除本地状态
|
||||
clearAuth();
|
||||
queryClient.clear();
|
||||
navigate('/login');
|
||||
},
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await mutation.mutateAsync();
|
||||
} catch (error) {
|
||||
// 即使登出失败也清除本地状态
|
||||
} finally {
|
||||
clearAuth();
|
||||
queryClient.clear();
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return { ...mutation, mutateAsync: logout };
|
||||
};
|
||||
|
||||
// 获取当前用户 Hook
|
||||
export const useCurrentUser = () => {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const setUser = useAuthStore((state) => state.setUser);
|
||||
|
||||
const query = useQuery<authApi.User>({
|
||||
return useQuery({
|
||||
queryKey: ['currentUser'],
|
||||
queryFn: authApi.getCurrentUser,
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
setUser(query.data);
|
||||
}
|
||||
}, [query.data, setUser]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
/**
|
||||
* 工作空间相关 Hooks
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import * as workspaceApi from '@/api/workspace';
|
||||
import { useWorkspaceStore } from '@/store/workspaceStore';
|
||||
|
||||
// 获取工作空间列表
|
||||
export const useWorkspaces = () => {
|
||||
const setWorkspaces = useWorkspaceStore((state) => state.setWorkspaces);
|
||||
|
||||
const query = useQuery<workspaceApi.Workspace[]>({
|
||||
return useQuery({
|
||||
queryKey: ['workspaces'],
|
||||
queryFn: workspaceApi.getWorkspaces,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
setWorkspaces(query.data);
|
||||
}
|
||||
}, [query.data, setWorkspaces]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
// 获取工作空间详情
|
||||
export const useWorkspace = (id: string) => {
|
||||
return useQuery<workspaceApi.Workspace>({
|
||||
return useQuery({
|
||||
queryKey: ['workspace', id],
|
||||
queryFn: () => workspaceApi.getWorkspace(id),
|
||||
enabled: !!id,
|
||||
@@ -36,20 +25,23 @@ export const useWorkspace = (id: string) => {
|
||||
// 创建工作空间
|
||||
export const useCreateWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const addWorkspace = useWorkspaceStore((state) => state.addWorkspace);
|
||||
|
||||
return useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: workspaceApi.createWorkspace,
|
||||
onSuccess: (data) => {
|
||||
addWorkspace(data);
|
||||
queryClient.invalidateQueries({ queryKey: ['workspaces'] });
|
||||
},
|
||||
});
|
||||
|
||||
const createWorkspace = async (data: Parameters<typeof workspaceApi.createWorkspace>[0]) => {
|
||||
const result = await mutation.mutateAsync(data);
|
||||
queryClient.invalidateQueries({ queryKey: ['workspaces'] });
|
||||
return result;
|
||||
};
|
||||
|
||||
return { ...mutation, mutateAsync: createWorkspace };
|
||||
};
|
||||
|
||||
// 获取成员列表
|
||||
export const useWorkspaceMembers = (workspaceId: string) => {
|
||||
return useQuery<workspaceApi.WorkspaceMember[]>({
|
||||
return useQuery({
|
||||
queryKey: ['workspaceMembers', workspaceId],
|
||||
queryFn: () => workspaceApi.getMembers(workspaceId),
|
||||
enabled: !!workspaceId,
|
||||
@@ -60,11 +52,16 @@ export const useWorkspaceMembers = (workspaceId: string) => {
|
||||
export const useInviteMember = (workspaceId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: workspaceApi.InviteMemberRequest) =>
|
||||
workspaceApi.inviteMember(workspaceId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workspaceMembers', workspaceId] });
|
||||
},
|
||||
});
|
||||
|
||||
const inviteMember = async (data: workspaceApi.InviteMemberRequest) => {
|
||||
const result = await mutation.mutateAsync(data);
|
||||
queryClient.invalidateQueries({ queryKey: ['workspaceMembers', workspaceId] });
|
||||
return result;
|
||||
};
|
||||
|
||||
return { ...mutation, mutateAsync: inviteMember };
|
||||
};
|
||||
|
||||
@@ -1,19 +1,104 @@
|
||||
/**
|
||||
* useAuth hooks smoke tests
|
||||
* useAuth Hook 单元测试
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useLogin, useLogout, useCurrentUser } from '@/hooks/useAuth';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useLogin, useRegister, useLogout } from '@/hooks/useAuth';
|
||||
import * as authApi from '@/api/auth';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
describe('auth hooks exports', () => {
|
||||
it('should expose useLogin', () => {
|
||||
expect(useLogin).toBeTypeOf('function');
|
||||
// Mock API
|
||||
vi.mock('@/api/auth');
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Test wrapper
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useAuth', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should expose useLogout', () => {
|
||||
expect(useLogout).toBeTypeOf('function');
|
||||
it('should login successfully', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'mock-token',
|
||||
refresh_token: 'refresh-token',
|
||||
token_type: 'bearer',
|
||||
user_id: '1',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
};
|
||||
|
||||
vi.mocked(authApi.login).mockResolvedValue(mockResponse);
|
||||
vi.mocked(authApi.getCurrentUser).mockResolvedValue({
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLogin(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeDefined();
|
||||
});
|
||||
|
||||
expect(authApi.login).toBeDefined();
|
||||
});
|
||||
|
||||
it('should expose useCurrentUser', () => {
|
||||
expect(useCurrentUser).toBeTypeOf('function');
|
||||
it('should register successfully', async () => {
|
||||
const mockResponse = {
|
||||
user_id: '1',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
message: '注册成功',
|
||||
};
|
||||
|
||||
vi.mocked(authApi.register).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useRegister(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeDefined();
|
||||
});
|
||||
|
||||
expect(authApi.register).toBeDefined();
|
||||
});
|
||||
|
||||
it('should logout successfully', async () => {
|
||||
localStorage.setItem('access_token', 'mock-token');
|
||||
|
||||
vi.mocked(authApi.logout).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useLogout(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeDefined();
|
||||
});
|
||||
|
||||
expect(authApi.logout).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { expect, afterEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import matchers from '@testing-library/jest-dom/matchers';
|
||||
import * as matchers from '@testing-library/jest-dom/matchers';
|
||||
|
||||
// 扩展 Vitest 的 expect
|
||||
expect.extend(matchers);
|
||||
@@ -12,3 +12,14 @@ expect.extend(matchers);
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// 全局类型声明
|
||||
declare global {
|
||||
namespace Vi {
|
||||
interface Assertion<T = any> extends jest.Matchers<void, T> {}
|
||||
interface AsymmetricMatchersContaining extends jest.Matchers<void, any> {}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出空对象以使文件成为模块
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user