Skip to content

Commit 6d1cfeb

Browse files
authored
Merge pull request #114 from tribeti/unit-test
unit test
2 parents 17b25c5 + ac6dcaa commit 6d1cfeb

19 files changed

Lines changed: 660 additions & 2413 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { checkEmailExistsAction, requestPasswordResetAction } from '@/app/actions/auth.actions';
2+
import { createClient } from '@/utils/supabase/server';
3+
4+
jest.mock('@/utils/supabase/server', () => ({
5+
createClient: jest.fn(),
6+
}));
7+
8+
describe('Auth Actions (Server Actions)', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
});
12+
13+
describe('checkEmailExistsAction', () => {
14+
it('returns error for invalid email', async () => {
15+
const result = await checkEmailExistsAction('invalid-email');
16+
expect(result.exists).toBe(false);
17+
expect(result.error).toBe('Email không đúng định dạng.');
18+
});
19+
20+
it('returns true if email exists by calling Supabase RPC', async () => {
21+
const mockRpc = jest.fn().mockResolvedValue({ data: true, error: null });
22+
(createClient as jest.Mock).mockResolvedValue({ rpc: mockRpc });
23+
24+
const result = await checkEmailExistsAction('test@gmail.com');
25+
expect(result.exists).toBe(true);
26+
expect(mockRpc).toHaveBeenCalledWith('check_email_exists', { email_to_check: 'test@gmail.com' });
27+
});
28+
});
29+
30+
describe('requestPasswordResetAction', () => {
31+
it('returns error for invalid email format', async () => {
32+
const result = await requestPasswordResetAction('bad-email');
33+
expect(result.error).toBe('Email không đúng định dạng.');
34+
});
35+
36+
it('returns success on valid email by calling Supabase Auth API', async () => {
37+
const mockReset = jest.fn().mockResolvedValue({ error: null });
38+
(createClient as jest.Mock).mockResolvedValue({
39+
auth: { resetPasswordForEmail: mockReset },
40+
});
41+
42+
const result = await requestPasswordResetAction('user@example.com');
43+
expect(result.success).toBe(true);
44+
expect(mockReset).toHaveBeenCalledWith('user@example.com', expect.any(Object));
45+
});
46+
});
47+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createTaskAction, deleteTaskAction } from '@/app/actions/kanban.actions';
2+
import { createClient } from '@/utils/supabase/server';
3+
import { verifyBoardAccess, verifyTaskAccess } from '@/utils/board-access';
4+
import { revalidatePath } from 'next/cache';
5+
6+
// Mocks
7+
jest.mock('@/utils/supabase/server', () => ({
8+
createClient: jest.fn(),
9+
}));
10+
11+
jest.mock('@/utils/supabase/admin', () => ({
12+
createAdminClient: jest.fn().mockReturnValue({
13+
from: jest.fn().mockReturnValue({
14+
insert: jest.fn(),
15+
})
16+
}),
17+
}));
18+
19+
jest.mock('@/utils/board-access', () => ({
20+
verifyBoardAccess: jest.fn(),
21+
verifyAllBoardsAccess: jest.fn(),
22+
verifyTaskAccess: jest.fn(),
23+
validateString: jest.fn((str) => str),
24+
}));
25+
26+
jest.mock('next/cache', () => ({
27+
revalidatePath: jest.fn(),
28+
}));
29+
30+
describe('Kanban / Task Server Actions Test', () => {
31+
let mockSupabase: any;
32+
33+
beforeEach(() => {
34+
jest.clearAllMocks();
35+
36+
mockSupabase = {
37+
auth: {
38+
getUser: jest.fn().mockResolvedValue({ data: { user: { id: 'user_123' } }, error: null }),
39+
},
40+
from: jest.fn().mockReturnThis(),
41+
select: jest.fn().mockReturnThis(),
42+
insert: jest.fn().mockReturnThis(),
43+
update: jest.fn().mockReturnThis(),
44+
delete: jest.fn().mockReturnThis(),
45+
eq: jest.fn().mockReturnThis(),
46+
single: jest.fn().mockResolvedValue({ data: { board_id: 1, id: 1 }, error: null }),
47+
};
48+
49+
(createClient as jest.Mock).mockResolvedValue(mockSupabase);
50+
});
51+
52+
describe('createTaskAction', () => {
53+
it('creates a task successfully and revalidates page cache', async () => {
54+
await createTaskAction({ column_id: 1, title: 'New Valid Task', position: 1 } as any);
55+
56+
// Verification logic passes
57+
expect(verifyBoardAccess).toHaveBeenCalled();
58+
59+
// Makes DB insertion calls
60+
expect(mockSupabase.from).toHaveBeenCalledWith('tasks');
61+
expect(mockSupabase.insert).toHaveBeenCalled();
62+
63+
// Updates Next.js Router cache
64+
expect(revalidatePath).toHaveBeenCalledWith('/projects');
65+
});
66+
67+
it('throws error if user is unauthenticated', async () => {
68+
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null }, error: new Error('Auth Err') });
69+
await expect(createTaskAction({ column_id: 1, title: 'Task' } as any)).rejects.toThrow('Unauthorized');
70+
});
71+
});
72+
73+
describe('deleteTaskAction', () => {
74+
it('deletes a task successfully with proper row level security simulation', async () => {
75+
await deleteTaskAction(10);
76+
expect(verifyTaskAccess).toHaveBeenCalledWith(expect.any(Object), 'user_123', 10);
77+
expect(mockSupabase.delete).toHaveBeenCalled();
78+
expect(revalidatePath).toHaveBeenCalledWith('/projects');
79+
});
80+
});
81+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import { useProjects } from '@/hooks/useProjects';
3+
import * as projectService from '@/services/project.service';
4+
5+
// Mocking dependencies
6+
jest.mock('@/services/project.service');
7+
jest.mock('sonner', () => ({
8+
toast: {
9+
success: jest.fn(),
10+
error: jest.fn(),
11+
warning: jest.fn(),
12+
}
13+
}));
14+
15+
describe('useProjects Hook', () => {
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
it('initially has no boards and is not loading if no userId is provided', async () => {
21+
const { result } = renderHook(() => useProjects());
22+
23+
expect(result.current.ownedBoards).toEqual([]);
24+
expect(result.current.joinedBoards).toEqual([]);
25+
expect(result.current.boardsLoading).toBe(false);
26+
});
27+
28+
it('fetches boards if userId is provided', async () => {
29+
const mockData = { ownedBoards: [{ id: 1, title: 'Test' } as any], joinedBoards: [] };
30+
jest.mocked(projectService.fetchUserBoards).mockResolvedValue(mockData);
31+
32+
const { result } = renderHook(() => useProjects('user123'));
33+
34+
await waitFor(() => {
35+
expect(result.current.boardsLoading).toBe(false);
36+
});
37+
38+
expect(projectService.fetchUserBoards).toHaveBeenCalled();
39+
expect(result.current.ownedBoards).toEqual(mockData.ownedBoards);
40+
});
41+
});

0 commit comments

Comments
 (0)