Add unit tests: auth store, API layer (tasks, habits, savings, profile), vitest config
All checks were successful
CI / ci (push) Successful in 33s
All checks were successful
CI / ci (push) Successful in 33s
This commit is contained in:
1739
package-lock.json
generated
1739
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -11,31 +11,36 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"axios": "^1.6.5",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"framer-motion": "^11.0.3",
|
||||
"lucide-react": "^0.312.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"axios": "^1.6.5",
|
||||
"zustand": "^4.5.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"lucide-react": "^0.312.0",
|
||||
"clsx": "^2.1.0",
|
||||
"framer-motion": "^11.0.3",
|
||||
"recharts": "^2.12.0"
|
||||
"recharts": "^2.12.0",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.5.0",
|
||||
"@storybook/addon-themes": "^8.5.0",
|
||||
"@storybook/blocks": "^8.5.0",
|
||||
"@storybook/react": "^8.5.0",
|
||||
"@storybook/react-vite": "^8.5.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"jsdom": "^28.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
"storybook": "^8.5.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.0.12",
|
||||
"@storybook/react": "^8.5.0",
|
||||
"@storybook/react-vite": "^8.5.0",
|
||||
"@storybook/addon-essentials": "^8.5.0",
|
||||
"@storybook/addon-themes": "^8.5.0",
|
||||
"@storybook/blocks": "^8.5.0",
|
||||
"storybook": "^8.5.0"
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
267
src/__tests__/api.test.js
Normal file
267
src/__tests__/api.test.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('../api/client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
interceptors: {
|
||||
request: { use: vi.fn() },
|
||||
response: { use: vi.fn() },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import api from '../api/client'
|
||||
import { tasksApi } from '../api/tasks'
|
||||
import { habitsApi } from '../api/habits'
|
||||
import { savingsApi } from '../api/savings'
|
||||
import { profileApi } from '../api/profile'
|
||||
|
||||
describe('tasksApi', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('should list all tasks', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [{ id: 1, title: 'Test' }] })
|
||||
const result = await tasksApi.list()
|
||||
expect(api.get).toHaveBeenCalledWith('tasks')
|
||||
expect(result).toEqual([{ id: 1, title: 'Test' }])
|
||||
})
|
||||
|
||||
it('should list completed tasks', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await tasksApi.list(true)
|
||||
expect(api.get).toHaveBeenCalledWith('tasks?completed=true')
|
||||
})
|
||||
|
||||
it('should list incomplete tasks', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await tasksApi.list(false)
|
||||
expect(api.get).toHaveBeenCalledWith('tasks?completed=false')
|
||||
})
|
||||
|
||||
it('should get today tasks', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await tasksApi.today()
|
||||
expect(api.get).toHaveBeenCalledWith('tasks/today')
|
||||
})
|
||||
|
||||
it('should create a task', async () => {
|
||||
const taskData = { title: 'New Task', priority: 1 }
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1, ...taskData } })
|
||||
const result = await tasksApi.create(taskData)
|
||||
expect(api.post).toHaveBeenCalledWith('tasks', taskData)
|
||||
expect(result.title).toBe('New Task')
|
||||
})
|
||||
|
||||
it('should update a task', async () => {
|
||||
api.put.mockResolvedValueOnce({ data: { id: 1, title: 'Updated' } })
|
||||
await tasksApi.update(1, { title: 'Updated' })
|
||||
expect(api.put).toHaveBeenCalledWith('tasks/1', { title: 'Updated' })
|
||||
})
|
||||
|
||||
it('should delete a task', async () => {
|
||||
api.delete.mockResolvedValueOnce({})
|
||||
await tasksApi.delete(1)
|
||||
expect(api.delete).toHaveBeenCalledWith('tasks/1')
|
||||
})
|
||||
|
||||
it('should complete a task', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1, completed: true } })
|
||||
await tasksApi.complete(1)
|
||||
expect(api.post).toHaveBeenCalledWith('tasks/1/complete')
|
||||
})
|
||||
|
||||
it('should uncomplete a task', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1, completed: false } })
|
||||
await tasksApi.uncomplete(1)
|
||||
expect(api.post).toHaveBeenCalledWith('tasks/1/uncomplete')
|
||||
})
|
||||
|
||||
it('should get a single task', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { id: 5, title: 'Task 5' } })
|
||||
const result = await tasksApi.get(5)
|
||||
expect(api.get).toHaveBeenCalledWith('tasks/5')
|
||||
expect(result.id).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('habitsApi', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('should list habits', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [{ id: 1, name: 'Exercise' }] })
|
||||
const result = await habitsApi.list()
|
||||
expect(api.get).toHaveBeenCalledWith('/habits')
|
||||
expect(result).toEqual([{ id: 1, name: 'Exercise' }])
|
||||
})
|
||||
|
||||
it('should create a habit', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1, name: 'Read' } })
|
||||
await habitsApi.create({ name: 'Read' })
|
||||
expect(api.post).toHaveBeenCalledWith('/habits', { name: 'Read' })
|
||||
})
|
||||
|
||||
it('should update a habit', async () => {
|
||||
api.put.mockResolvedValueOnce({ data: { id: 1, name: 'Updated' } })
|
||||
await habitsApi.update(1, { name: 'Updated' })
|
||||
expect(api.put).toHaveBeenCalledWith('/habits/1', { name: 'Updated' })
|
||||
})
|
||||
|
||||
it('should log a habit', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await habitsApi.log(5, { date: '2025-03-01' })
|
||||
expect(api.post).toHaveBeenCalledWith('/habits/5/log', { date: '2025-03-01' })
|
||||
})
|
||||
|
||||
it('should log habit with empty data', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await habitsApi.log(5)
|
||||
expect(api.post).toHaveBeenCalledWith('/habits/5/log', {})
|
||||
})
|
||||
|
||||
it('should get logs with custom days', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await habitsApi.getLogs(5, 60)
|
||||
expect(api.get).toHaveBeenCalledWith('/habits/5/logs?days=60')
|
||||
})
|
||||
|
||||
it('should get overall stats', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { total_habits: 5 } })
|
||||
await habitsApi.getStats()
|
||||
expect(api.get).toHaveBeenCalledWith('/habits/stats')
|
||||
})
|
||||
|
||||
it('should get single habit stats', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { habit_id: 3 } })
|
||||
await habitsApi.getHabitStats(3)
|
||||
expect(api.get).toHaveBeenCalledWith('/habits/3/stats')
|
||||
})
|
||||
|
||||
it('should get freezes', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await habitsApi.getFreezes(3)
|
||||
expect(api.get).toHaveBeenCalledWith('/habits/3/freezes')
|
||||
})
|
||||
|
||||
it('should add freeze', async () => {
|
||||
const freezeData = { start_date: '2025-03-01', end_date: '2025-03-07' }
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await habitsApi.addFreeze(3, freezeData)
|
||||
expect(api.post).toHaveBeenCalledWith('/habits/3/freezes', freezeData)
|
||||
})
|
||||
|
||||
it('should delete freeze', async () => {
|
||||
api.delete.mockResolvedValueOnce({})
|
||||
await habitsApi.deleteFreeze(3, 10)
|
||||
expect(api.delete).toHaveBeenCalledWith('/habits/3/freezes/10')
|
||||
})
|
||||
|
||||
it('should delete a habit', async () => {
|
||||
api.delete.mockResolvedValueOnce({})
|
||||
await habitsApi.delete(5)
|
||||
expect(api.delete).toHaveBeenCalledWith('/habits/5')
|
||||
})
|
||||
|
||||
it('should delete a log', async () => {
|
||||
api.delete.mockResolvedValueOnce({})
|
||||
await habitsApi.deleteLog(3, 7)
|
||||
expect(api.delete).toHaveBeenCalledWith('/habits/3/logs/7')
|
||||
})
|
||||
})
|
||||
|
||||
describe('savingsApi', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('should list categories', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.listCategories()
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/categories')
|
||||
})
|
||||
|
||||
it('should get single category', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await savingsApi.getCategory(1)
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/categories/1')
|
||||
})
|
||||
|
||||
it('should create category', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await savingsApi.createCategory({ name: 'Savings' })
|
||||
expect(api.post).toHaveBeenCalledWith('/savings/categories', { name: 'Savings' })
|
||||
})
|
||||
|
||||
it('should update category', async () => {
|
||||
api.put.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await savingsApi.updateCategory(1, { name: 'Updated' })
|
||||
expect(api.put).toHaveBeenCalledWith('/savings/categories/1', { name: 'Updated' })
|
||||
})
|
||||
|
||||
it('should delete category', async () => {
|
||||
api.delete.mockResolvedValueOnce({})
|
||||
await savingsApi.deleteCategory(1)
|
||||
expect(api.delete).toHaveBeenCalledWith('/savings/categories/1')
|
||||
})
|
||||
|
||||
it('should create transaction', async () => {
|
||||
const data = { category_id: 1, amount: 1000, type: 'deposit', date: '2025-03-01' }
|
||||
api.post.mockResolvedValueOnce({ data: { id: 1 } })
|
||||
await savingsApi.createTransaction(data)
|
||||
expect(api.post).toHaveBeenCalledWith('/savings/transactions', data)
|
||||
})
|
||||
|
||||
it('should list transactions with category filter', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.listTransactions(5, 50, 10)
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/transactions?limit=50&offset=10&category_id=5')
|
||||
})
|
||||
|
||||
it('should list transactions without category filter', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.listTransactions(null)
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/transactions?limit=100&offset=0')
|
||||
})
|
||||
|
||||
it('should get stats', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { total_balance: 5000 } })
|
||||
await savingsApi.getStats()
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/stats')
|
||||
})
|
||||
|
||||
it('should get members', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.getMembers(1)
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/categories/1/members')
|
||||
})
|
||||
|
||||
it('should add member', async () => {
|
||||
api.post.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.addMember(1, 42)
|
||||
expect(api.post).toHaveBeenCalledWith('/savings/categories/1/members', { user_id: 42 })
|
||||
})
|
||||
|
||||
it('should get recurring plans', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: [] })
|
||||
await savingsApi.getRecurringPlans(1)
|
||||
expect(api.get).toHaveBeenCalledWith('/savings/categories/1/recurring-plans')
|
||||
})
|
||||
})
|
||||
|
||||
describe('profileApi', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('should get profile', async () => {
|
||||
api.get.mockResolvedValueOnce({ data: { username: 'test' } })
|
||||
const result = await profileApi.get()
|
||||
expect(api.get).toHaveBeenCalledWith('/profile')
|
||||
expect(result.username).toBe('test')
|
||||
})
|
||||
|
||||
it('should update profile', async () => {
|
||||
api.put.mockResolvedValueOnce({ data: { username: 'updated' } })
|
||||
const result = await profileApi.update({ username: 'updated' })
|
||||
expect(api.put).toHaveBeenCalledWith('/profile', { username: 'updated' })
|
||||
expect(result.username).toBe('updated')
|
||||
})
|
||||
})
|
||||
134
src/__tests__/auth.store.test.js
Normal file
134
src/__tests__/auth.store.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('../api/client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
interceptors: {
|
||||
request: { use: vi.fn() },
|
||||
response: { use: vi.fn() },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import api from '../api/client'
|
||||
import { useAuthStore } from '../store/auth'
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
beforeEach(() => {
|
||||
useAuthStore.setState({
|
||||
user: null,
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
})
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should have correct initial state', () => {
|
||||
const state = useAuthStore.getState()
|
||||
expect(state.user).toBeNull()
|
||||
expect(state.isLoading).toBe(true)
|
||||
expect(state.isAuthenticated).toBe(false)
|
||||
})
|
||||
|
||||
it('should login successfully', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
user: { id: 1, email: 'test@test.com', username: 'test' },
|
||||
access_token: 'access-123',
|
||||
refresh_token: 'refresh-123',
|
||||
},
|
||||
}
|
||||
api.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
await useAuthStore.getState().login('test@test.com', 'password')
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith('/auth/login', {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
})
|
||||
expect(localStorage.getItem('access_token')).toBe('access-123')
|
||||
expect(localStorage.getItem('refresh_token')).toBe('refresh-123')
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(true)
|
||||
expect(useAuthStore.getState().user).toEqual(mockResponse.data.user)
|
||||
})
|
||||
|
||||
it('should register successfully', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
user: { id: 2, email: 'new@test.com', username: 'newuser' },
|
||||
access_token: 'access-456',
|
||||
refresh_token: 'refresh-456',
|
||||
},
|
||||
}
|
||||
api.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
await useAuthStore.getState().register('new@test.com', 'newuser', 'password123')
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith('/auth/register', {
|
||||
email: 'new@test.com',
|
||||
username: 'newuser',
|
||||
password: 'password123',
|
||||
})
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(true)
|
||||
expect(localStorage.getItem('access_token')).toBe('access-456')
|
||||
})
|
||||
|
||||
it('should logout and clear tokens', () => {
|
||||
localStorage.setItem('access_token', 'old-token')
|
||||
localStorage.setItem('refresh_token', 'old-refresh')
|
||||
useAuthStore.setState({ user: { id: 1 }, isAuthenticated: true })
|
||||
|
||||
useAuthStore.getState().logout()
|
||||
|
||||
expect(localStorage.getItem('access_token')).toBeNull()
|
||||
expect(localStorage.getItem('refresh_token')).toBeNull()
|
||||
expect(useAuthStore.getState().user).toBeNull()
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with no token', async () => {
|
||||
await useAuthStore.getState().initialize()
|
||||
|
||||
expect(useAuthStore.getState().isLoading).toBe(false)
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with valid token', async () => {
|
||||
localStorage.setItem('access_token', 'valid-token')
|
||||
api.get.mockResolvedValueOnce({
|
||||
data: { id: 1, email: 'test@test.com', username: 'test' },
|
||||
})
|
||||
|
||||
await useAuthStore.getState().initialize()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/auth/me')
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(true)
|
||||
expect(useAuthStore.getState().isLoading).toBe(false)
|
||||
expect(useAuthStore.getState().user.email).toBe('test@test.com')
|
||||
})
|
||||
|
||||
it('should handle invalid token on initialize', async () => {
|
||||
localStorage.setItem('access_token', 'invalid-token')
|
||||
api.get.mockRejectedValueOnce(new Error('401'))
|
||||
|
||||
await useAuthStore.getState().initialize()
|
||||
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false)
|
||||
expect(localStorage.getItem('access_token')).toBeNull()
|
||||
expect(localStorage.getItem('refresh_token')).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle login error', async () => {
|
||||
api.post.mockRejectedValueOnce(new Error('Invalid credentials'))
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().login('bad@test.com', 'wrong')
|
||||
).rejects.toThrow('Invalid credentials')
|
||||
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false)
|
||||
})
|
||||
})
|
||||
1
src/__tests__/setup.js
Normal file
1
src/__tests__/setup.js
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom'
|
||||
11
vitest.config.js
Normal file
11
vitest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: './src/__tests__/setup.js',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user