shawnxixi-cli 0.3.0 → 1.1.0
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/.env.example +8 -2
- package/.github/workflows/ci.yml +9 -22
- package/.github/workflows/release.yml +30 -0
- package/CLAUDE.md +167 -63
- package/README.md +345 -65
- package/commitlint.config.js +10 -0
- package/dist/commands/biz/calendar.d.ts +17 -2
- package/dist/commands/biz/calendar.d.ts.map +1 -1
- package/dist/commands/biz/calendar.js +47 -9
- package/dist/commands/biz/calendar.js.map +1 -1
- package/dist/commands/biz/finance.d.ts +32 -1
- package/dist/commands/biz/finance.d.ts.map +1 -1
- package/dist/commands/biz/finance.js +99 -12
- package/dist/commands/biz/finance.js.map +1 -1
- package/dist/commands/biz/health.d.ts +32 -0
- package/dist/commands/biz/health.d.ts.map +1 -0
- package/dist/commands/biz/health.js +92 -0
- package/dist/commands/biz/health.js.map +1 -0
- package/dist/commands/biz/item.d.ts +41 -0
- package/dist/commands/biz/item.d.ts.map +1 -1
- package/dist/commands/biz/item.js +89 -5
- package/dist/commands/biz/item.js.map +1 -1
- package/dist/commands/biz/record.d.ts +0 -8
- package/dist/commands/biz/record.d.ts.map +1 -1
- package/dist/commands/biz/record.js +29 -8
- package/dist/commands/biz/record.js.map +1 -1
- package/dist/commands/biz/task.d.ts +38 -0
- package/dist/commands/biz/task.d.ts.map +1 -0
- package/dist/commands/biz/task.js +110 -0
- package/dist/commands/biz/task.js.map +1 -0
- package/dist/commands/biz/todo.d.ts +52 -8
- package/dist/commands/biz/todo.d.ts.map +1 -1
- package/dist/commands/biz/todo.js +167 -17
- package/dist/commands/biz/todo.js.map +1 -1
- package/dist/commands/sys/health.d.ts.map +1 -1
- package/dist/commands/sys/health.js +20 -3
- package/dist/commands/sys/health.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +281 -42
- package/dist/index.js.map +1 -1
- package/dist/services/api.d.ts +22 -8
- package/dist/services/api.d.ts.map +1 -1
- package/dist/services/api.js +108 -28
- package/dist/services/api.js.map +1 -1
- package/dist/utils/env.d.ts +16 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +78 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/finance.model.d.ts +80 -0
- package/dist/utils/finance.model.d.ts.map +1 -0
- package/dist/utils/finance.model.js +150 -0
- package/dist/utils/finance.model.js.map +1 -0
- package/dist/utils/output.d.ts +42 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +124 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/todo.model.d.ts +55 -0
- package/dist/utils/todo.model.d.ts.map +1 -0
- package/dist/utils/todo.model.js +135 -0
- package/dist/utils/todo.model.js.map +1 -0
- package/package.json +18 -2
- package/src/commands/biz/calendar.ts +61 -10
- package/src/commands/biz/finance.ts +112 -13
- package/src/commands/biz/health.ts +96 -0
- package/src/commands/biz/item.ts +113 -6
- package/src/commands/biz/record.ts +32 -8
- package/src/commands/biz/task.ts +115 -0
- package/src/commands/biz/todo.ts +193 -21
- package/src/commands/sys/health.ts +23 -3
- package/src/index.ts +311 -54
- package/src/services/api.ts +111 -30
- package/src/utils/env.ts +85 -0
- package/src/utils/finance.model.ts +182 -0
- package/src/utils/output.ts +126 -0
- package/src/utils/todo.model.ts +167 -0
- package/tests/commands/finance.test.ts +281 -0
- package/tests/commands/item.test.ts +215 -0
- package/tests/commands/todo.test.ts +292 -9
- package/tests/services/api.test.ts +292 -20
- package/tests/utils/finance.model.test.ts +319 -0
- package/tests/utils/todo.model.test.ts +315 -0
|
@@ -1,39 +1,322 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Todo Command Tests
|
|
3
|
+
* Comprehensive CLI integration tests for biz todo commands
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { todoCommands } from '../../src/commands/biz/todo';
|
|
7
|
+
import { setGlobalFlags } from '../../src/utils/output';
|
|
6
8
|
|
|
7
9
|
// Mock apiService
|
|
8
10
|
jest.mock('../../src/services/api', () => ({
|
|
9
11
|
apiService: {
|
|
10
12
|
createTodo: jest.fn().mockResolvedValue({ id: 1, title: '测试待办' }),
|
|
11
|
-
listTodos: jest.fn().mockResolvedValue([{ id: 1, title: '测试待办' }]),
|
|
13
|
+
listTodos: jest.fn().mockResolvedValue({ data: [{ id: 1, title: '测试待办' }] }),
|
|
14
|
+
updateTodo: jest.fn().mockResolvedValue({ id: 1, title: '更新后的待办' }),
|
|
15
|
+
deleteTodo: jest.fn().mockResolvedValue({ success: true }),
|
|
16
|
+
repeatTodo: jest.fn().mockResolvedValue({ id: 2, title: '下次 occurrence' }),
|
|
17
|
+
listSubtasks: jest.fn().mockResolvedValue({ data: [] }),
|
|
12
18
|
},
|
|
13
19
|
}));
|
|
14
20
|
|
|
15
21
|
import { apiService } from '../../src/services/api';
|
|
16
22
|
|
|
23
|
+
// Mock process.exit to prevent test from exiting
|
|
24
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
25
|
+
|
|
26
|
+
// Suppress console output during tests
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
setGlobalFlags({ quiet: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
setGlobalFlags({ quiet: false });
|
|
33
|
+
mockExit.mockRestore();
|
|
34
|
+
});
|
|
35
|
+
|
|
17
36
|
describe('biz todo commands', () => {
|
|
18
37
|
beforeEach(() => {
|
|
19
38
|
jest.clearAllMocks();
|
|
20
39
|
});
|
|
21
40
|
|
|
22
41
|
describe('create', () => {
|
|
23
|
-
it('should call apiService.createTodo', async () => {
|
|
24
|
-
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
42
|
+
it('should call apiService.createTodo with title only', async () => {
|
|
25
43
|
await todoCommands.create('测试待办');
|
|
26
|
-
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
27
|
-
|
|
44
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
45
|
+
'测试待办',
|
|
46
|
+
undefined, // due
|
|
47
|
+
undefined, // priority
|
|
48
|
+
undefined, // tags
|
|
49
|
+
undefined, // repeat
|
|
50
|
+
undefined, // endDate
|
|
51
|
+
undefined // parentId
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should call apiService.createTodo with due date', async () => {
|
|
56
|
+
await todoCommands.create('测试待办', { due: '2026-04-15' });
|
|
57
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
58
|
+
'测试待办',
|
|
59
|
+
'2026-04-15',
|
|
60
|
+
undefined,
|
|
61
|
+
undefined,
|
|
62
|
+
undefined,
|
|
63
|
+
undefined,
|
|
64
|
+
undefined
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should call apiService.createTodo with priority', async () => {
|
|
69
|
+
await todoCommands.create('测试待办', { priority: 'high' });
|
|
70
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
71
|
+
'测试待办',
|
|
72
|
+
undefined,
|
|
73
|
+
'high',
|
|
74
|
+
undefined,
|
|
75
|
+
undefined,
|
|
76
|
+
undefined,
|
|
77
|
+
undefined
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should call apiService.createTodo with tags', async () => {
|
|
82
|
+
await todoCommands.create('测试待办', { tags: 'work,important' });
|
|
83
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
84
|
+
'测试待办',
|
|
85
|
+
undefined,
|
|
86
|
+
undefined,
|
|
87
|
+
'work,important',
|
|
88
|
+
undefined,
|
|
89
|
+
undefined,
|
|
90
|
+
undefined
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should call apiService.createTodo with repeat rule', async () => {
|
|
95
|
+
await todoCommands.create('测试待办', { repeat: 'daily' });
|
|
96
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
97
|
+
'测试待办',
|
|
98
|
+
undefined,
|
|
99
|
+
undefined,
|
|
100
|
+
undefined,
|
|
101
|
+
'daily',
|
|
102
|
+
undefined,
|
|
103
|
+
undefined
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should call apiService.createTodo with end date', async () => {
|
|
108
|
+
await todoCommands.create('测试待办', { endDate: '2026-12-31' });
|
|
109
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
110
|
+
'测试待办',
|
|
111
|
+
undefined,
|
|
112
|
+
undefined,
|
|
113
|
+
undefined,
|
|
114
|
+
undefined,
|
|
115
|
+
'2026-12-31',
|
|
116
|
+
undefined
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should call apiService.createTodo with all options', async () => {
|
|
121
|
+
await todoCommands.create('测试待办', {
|
|
122
|
+
due: '2026-04-15',
|
|
123
|
+
priority: 'high',
|
|
124
|
+
tags: 'work,urgent',
|
|
125
|
+
repeat: 'weekly',
|
|
126
|
+
endDate: '2026-12-31',
|
|
127
|
+
});
|
|
128
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
129
|
+
'测试待办',
|
|
130
|
+
'2026-04-15',
|
|
131
|
+
'high',
|
|
132
|
+
'work,urgent',
|
|
133
|
+
'weekly',
|
|
134
|
+
'2026-12-31',
|
|
135
|
+
undefined
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should handle api errors gracefully', async () => {
|
|
140
|
+
(apiService.createTodo as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
141
|
+
|
|
142
|
+
await todoCommands.create('测试待办');
|
|
143
|
+
|
|
144
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
28
145
|
});
|
|
29
146
|
});
|
|
30
147
|
|
|
31
148
|
describe('list', () => {
|
|
32
|
-
it('should call apiService.listTodos', async () => {
|
|
33
|
-
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
149
|
+
it('should call apiService.listTodos without filters', async () => {
|
|
34
150
|
await todoCommands.list();
|
|
35
|
-
expect(apiService.listTodos).toHaveBeenCalledWith(undefined);
|
|
36
|
-
|
|
151
|
+
expect(apiService.listTodos).toHaveBeenCalledWith(undefined, undefined, undefined);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should call apiService.listTodos with status filter', async () => {
|
|
155
|
+
await todoCommands.list({ status: 'pending' });
|
|
156
|
+
expect(apiService.listTodos).toHaveBeenCalledWith('pending', undefined, undefined);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should call apiService.listTodos with priority filter', async () => {
|
|
160
|
+
await todoCommands.list({ priority: 'high' });
|
|
161
|
+
expect(apiService.listTodos).toHaveBeenCalledWith(undefined, 'high', undefined);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should call apiService.listTodos with tags filter', async () => {
|
|
165
|
+
await todoCommands.list({ tags: 'work' });
|
|
166
|
+
expect(apiService.listTodos).toHaveBeenCalledWith(undefined, undefined, 'work');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should call apiService.listTodos with all filters', async () => {
|
|
170
|
+
await todoCommands.list({ status: 'done', priority: 'low', tags: 'home' });
|
|
171
|
+
expect(apiService.listTodos).toHaveBeenCalledWith('done', 'low', 'home');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle empty results', async () => {
|
|
175
|
+
(apiService.listTodos as jest.Mock).mockResolvedValueOnce({ data: [] });
|
|
176
|
+
|
|
177
|
+
await todoCommands.list();
|
|
178
|
+
|
|
179
|
+
expect(apiService.listTodos).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle api errors gracefully', async () => {
|
|
183
|
+
(apiService.listTodos as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));
|
|
184
|
+
|
|
185
|
+
await todoCommands.list();
|
|
186
|
+
|
|
187
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('update', () => {
|
|
192
|
+
it('should call apiService.updateTodo with id and title', async () => {
|
|
193
|
+
await todoCommands.update(1, { title: '新标题' });
|
|
194
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { title: '新标题' });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should call apiService.updateTodo with status', async () => {
|
|
198
|
+
await todoCommands.update(1, { status: 'done' });
|
|
199
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { status: 'done' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should call apiService.updateTodo with priority', async () => {
|
|
203
|
+
await todoCommands.update(1, { priority: 'high' });
|
|
204
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { priority: 'high' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should call apiService.updateTodo with due date', async () => {
|
|
208
|
+
await todoCommands.update(1, { due: '2026-04-20' });
|
|
209
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { dueDate: '2026-04-20' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should call apiService.updateTodo with tags', async () => {
|
|
213
|
+
await todoCommands.update(1, { tags: 'work,urgent' });
|
|
214
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { tags: 'work,urgent' });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should call apiService.updateTodo with multiple updates', async () => {
|
|
218
|
+
await todoCommands.update(1, {
|
|
219
|
+
title: '新标题',
|
|
220
|
+
status: 'done',
|
|
221
|
+
priority: 'high',
|
|
222
|
+
due: '2026-04-20',
|
|
223
|
+
tags: 'work',
|
|
224
|
+
});
|
|
225
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, {
|
|
226
|
+
title: '新标题',
|
|
227
|
+
status: 'done',
|
|
228
|
+
priority: 'high',
|
|
229
|
+
dueDate: '2026-04-20',
|
|
230
|
+
tags: 'work',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should handle api errors gracefully', async () => {
|
|
235
|
+
(apiService.updateTodo as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
236
|
+
|
|
237
|
+
await todoCommands.update(1, { title: '新标题' });
|
|
238
|
+
|
|
239
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('done', () => {
|
|
244
|
+
it('should call apiService.updateTodo with id and done status', async () => {
|
|
245
|
+
await todoCommands.done(1);
|
|
246
|
+
expect(apiService.updateTodo).toHaveBeenCalledWith(1, { status: 'done' });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle api errors gracefully', async () => {
|
|
250
|
+
(apiService.updateTodo as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
251
|
+
|
|
252
|
+
await todoCommands.done(1);
|
|
253
|
+
|
|
254
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('delete', () => {
|
|
259
|
+
it('should call apiService.deleteTodo with id', async () => {
|
|
260
|
+
await todoCommands.delete(1);
|
|
261
|
+
expect(apiService.deleteTodo).toHaveBeenCalledWith(1);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle api errors gracefully', async () => {
|
|
265
|
+
(apiService.deleteTodo as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
266
|
+
|
|
267
|
+
await todoCommands.delete(1);
|
|
268
|
+
|
|
269
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('repeat', () => {
|
|
274
|
+
it('should call apiService.repeatTodo with id', async () => {
|
|
275
|
+
await todoCommands.repeat(1);
|
|
276
|
+
expect(apiService.repeatTodo).toHaveBeenCalledWith(1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle api errors gracefully', async () => {
|
|
280
|
+
(apiService.repeatTodo as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
281
|
+
|
|
282
|
+
await todoCommands.repeat(1);
|
|
283
|
+
|
|
284
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('subtask', () => {
|
|
289
|
+
it('should call apiService.createTodo with parentId', async () => {
|
|
290
|
+
await todoCommands.subtask(1, { title: '子待办标题' });
|
|
291
|
+
expect(apiService.createTodo).toHaveBeenCalledWith(
|
|
292
|
+
'子待办标题',
|
|
293
|
+
undefined,
|
|
294
|
+
undefined,
|
|
295
|
+
undefined,
|
|
296
|
+
undefined,
|
|
297
|
+
undefined,
|
|
298
|
+
'1'
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should exit with error when title is missing', async () => {
|
|
303
|
+
await todoCommands.subtask(1, {});
|
|
304
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('subtasks', () => {
|
|
309
|
+
it('should call apiService.listSubtasks with parentId', async () => {
|
|
310
|
+
await todoCommands.subtasks(1);
|
|
311
|
+
expect(apiService.listSubtasks).toHaveBeenCalledWith(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle api errors gracefully', async () => {
|
|
315
|
+
(apiService.listSubtasks as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
316
|
+
|
|
317
|
+
await todoCommands.subtasks(1);
|
|
318
|
+
|
|
319
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
37
320
|
});
|
|
38
321
|
});
|
|
39
322
|
});
|
|
@@ -1,39 +1,311 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Service Tests with Mock
|
|
3
|
+
* Comprehensive tests for HTTP mocking, error handling, and request serialization
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import axios from 'axios';
|
|
6
7
|
|
|
7
|
-
// Mock axios
|
|
8
|
-
jest.mock('axios', () =>
|
|
9
|
-
|
|
8
|
+
// Mock axios before importing apiService
|
|
9
|
+
jest.mock('axios', () => {
|
|
10
|
+
const mockAxiosInstance = {
|
|
10
11
|
interceptors: {
|
|
11
|
-
request: {
|
|
12
|
-
|
|
12
|
+
request: {
|
|
13
|
+
use: jest.fn((cb) => cb),
|
|
14
|
+
},
|
|
15
|
+
response: {
|
|
16
|
+
use: jest.fn((successCb, _errorCb) => successCb),
|
|
17
|
+
},
|
|
13
18
|
},
|
|
14
|
-
get: jest.fn()
|
|
15
|
-
post: jest.fn()
|
|
16
|
-
put: jest.fn()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
19
|
+
get: jest.fn(),
|
|
20
|
+
post: jest.fn(),
|
|
21
|
+
put: jest.fn(),
|
|
22
|
+
patch: jest.fn(),
|
|
23
|
+
delete: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
create: jest.fn(() => mockAxiosInstance),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Import after mocking
|
|
32
|
+
import { apiService } from '../../src/services/api';
|
|
20
33
|
|
|
21
34
|
describe('ApiService', () => {
|
|
35
|
+
const mockAxios = axios.create();
|
|
36
|
+
|
|
22
37
|
beforeEach(() => {
|
|
23
38
|
jest.clearAllMocks();
|
|
24
39
|
});
|
|
25
40
|
|
|
26
|
-
describe('
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
describe('Todo API', () => {
|
|
42
|
+
describe('createTodo', () => {
|
|
43
|
+
it('should call POST /api/v1/todos with correct payload', async () => {
|
|
44
|
+
const mockResponse = { data: { id: 1, title: '测试待办' } };
|
|
45
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
46
|
+
|
|
47
|
+
const result = await apiService.createTodo('测试待办', '2026-04-15', 'high', 'work,important');
|
|
48
|
+
|
|
49
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
50
|
+
'/api/v1/todos',
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
title: '测试待办',
|
|
53
|
+
dueDate: '2026-04-15T00:00:00',
|
|
54
|
+
priority: 'high',
|
|
55
|
+
status: 'pending',
|
|
56
|
+
tags: 'work,important',
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
expect(result).toEqual(mockResponse.data);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should use default priority when not specified', async () => {
|
|
63
|
+
const mockResponse = { data: { id: 2, title: '测试' } };
|
|
64
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
65
|
+
|
|
66
|
+
await apiService.createTodo('测试');
|
|
67
|
+
|
|
68
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
69
|
+
'/api/v1/todos',
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
priority: 'medium',
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle datetime format in due date', async () => {
|
|
77
|
+
const mockResponse = { data: { id: 3 } };
|
|
78
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
79
|
+
|
|
80
|
+
await apiService.createTodo('测试', '2026-04-15 14:30');
|
|
81
|
+
|
|
82
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
83
|
+
'/api/v1/todos',
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
dueDate: '2026-04-15T14:30:00',
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('listTodos', () => {
|
|
92
|
+
it('should call GET /api/v1/todos without params', async () => {
|
|
93
|
+
const mockResponse = { data: [] };
|
|
94
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
95
|
+
|
|
96
|
+
await apiService.listTodos();
|
|
97
|
+
|
|
98
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/todos', { params: {} });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should call GET with query params', async () => {
|
|
102
|
+
const mockResponse = { data: [] };
|
|
103
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
104
|
+
|
|
105
|
+
await apiService.listTodos('pending', 'high', 'work');
|
|
106
|
+
|
|
107
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/todos', {
|
|
108
|
+
params: { status: 'pending', priority: 'high', tags: 'work' },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should filter out undefined params', async () => {
|
|
113
|
+
const mockResponse = { data: [] };
|
|
114
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
115
|
+
|
|
116
|
+
await apiService.listTodos(undefined, 'high', undefined);
|
|
117
|
+
|
|
118
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/todos', {
|
|
119
|
+
params: { priority: 'high' },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('updateTodo', () => {
|
|
125
|
+
it('should call PATCH /api/v1/todos/:id with updates', async () => {
|
|
126
|
+
const mockResponse = { data: { id: 1, title: '更新后的标题' } };
|
|
127
|
+
(mockAxios.patch as jest.Mock).mockResolvedValue(mockResponse);
|
|
128
|
+
|
|
129
|
+
const result = await apiService.updateTodo(1, { title: '更新后的标题', status: 'done' });
|
|
130
|
+
|
|
131
|
+
expect(mockAxios.patch).toHaveBeenCalledWith('/api/v1/todos/1', {
|
|
132
|
+
title: '更新后的标题',
|
|
133
|
+
status: 'done',
|
|
134
|
+
});
|
|
135
|
+
expect(result).toEqual(mockResponse.data);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('deleteTodo', () => {
|
|
140
|
+
it('should call DELETE /api/v1/todos/:id', async () => {
|
|
141
|
+
const mockResponse = { data: { success: true } };
|
|
142
|
+
(mockAxios.delete as jest.Mock).mockResolvedValue(mockResponse);
|
|
143
|
+
|
|
144
|
+
await apiService.deleteTodo(1);
|
|
145
|
+
|
|
146
|
+
expect(mockAxios.delete).toHaveBeenCalledWith('/api/v1/todos/1');
|
|
147
|
+
});
|
|
30
148
|
});
|
|
31
149
|
});
|
|
32
150
|
|
|
33
|
-
describe('
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
151
|
+
describe('Finance API', () => {
|
|
152
|
+
describe('createFinance', () => {
|
|
153
|
+
it('should call POST /api/v1/finances with correct payload', async () => {
|
|
154
|
+
const mockResponse = { data: { id: 1, amount: 100, type: 'expense' } };
|
|
155
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
156
|
+
|
|
157
|
+
const result = await apiService.createFinance(100, 'expense', 'food', '午饭');
|
|
158
|
+
|
|
159
|
+
expect(mockAxios.post).toHaveBeenCalledWith('/api/v1/finances', {
|
|
160
|
+
amount: 100,
|
|
161
|
+
type: 'expense',
|
|
162
|
+
category: 'food',
|
|
163
|
+
description: '午饭',
|
|
164
|
+
});
|
|
165
|
+
expect(result).toEqual(mockResponse.data);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('listFinances', () => {
|
|
170
|
+
it('should call GET /api/v1/finances with filters', async () => {
|
|
171
|
+
const mockResponse = { data: [] };
|
|
172
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
173
|
+
|
|
174
|
+
await apiService.listFinances('expense', 'food', '2026-04');
|
|
175
|
+
|
|
176
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/finances', {
|
|
177
|
+
params: { type: 'expense', category: 'food', month: '2026-04' },
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('getFinanceStats', () => {
|
|
183
|
+
it('should call GET /api/v1/finances/stats with month', async () => {
|
|
184
|
+
const mockResponse = { data: { totalIncome: 10000, totalExpense: 5000 } };
|
|
185
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
186
|
+
|
|
187
|
+
const result = await apiService.getFinanceStats('2026-04');
|
|
188
|
+
|
|
189
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/finances/stats', {
|
|
190
|
+
params: { month: '2026-04' },
|
|
191
|
+
});
|
|
192
|
+
expect(result).toEqual(mockResponse.data);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Item API', () => {
|
|
198
|
+
describe('createItem', () => {
|
|
199
|
+
it('should call POST /api/v1/items with correct payload', async () => {
|
|
200
|
+
const mockResponse = { data: { id: 1, name: '测试物品' } };
|
|
201
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
202
|
+
|
|
203
|
+
await apiService.createItem('测试物品', 'electronics', '客厅', '2026-12-31');
|
|
204
|
+
|
|
205
|
+
expect(mockAxios.post).toHaveBeenCalledWith('/api/v1/items', {
|
|
206
|
+
name: '测试物品',
|
|
207
|
+
category: 'electronics',
|
|
208
|
+
location: '客厅',
|
|
209
|
+
expireDate: '2026-12-31T00:00:00',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('listItems', () => {
|
|
215
|
+
it('should call GET /api/v1/items with category filter', async () => {
|
|
216
|
+
const mockResponse = { data: [] };
|
|
217
|
+
(mockAxios.get as jest.Mock).mockResolvedValue(mockResponse);
|
|
218
|
+
|
|
219
|
+
await apiService.listItems('electronics');
|
|
220
|
+
|
|
221
|
+
expect(mockAxios.get).toHaveBeenCalledWith('/api/v1/items', {
|
|
222
|
+
params: { category: 'electronics' },
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('Error Handling', () => {
|
|
229
|
+
it('should handle network errors', async () => {
|
|
230
|
+
const error = new Error('Network Error');
|
|
231
|
+
(mockAxios.get as jest.Mock).mockRejectedValue(error);
|
|
232
|
+
|
|
233
|
+
await expect(apiService.listTodos()).rejects.toThrow('Network Error');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle HTTP 500 errors', async () => {
|
|
237
|
+
const error = {
|
|
238
|
+
response: {
|
|
239
|
+
status: 500,
|
|
240
|
+
data: { message: 'Internal Server Error' },
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
(mockAxios.get as jest.Mock).mockRejectedValue(error);
|
|
244
|
+
|
|
245
|
+
await expect(apiService.listTodos()).rejects.toEqual(error);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should handle HTTP 404 errors', async () => {
|
|
249
|
+
const error = {
|
|
250
|
+
response: {
|
|
251
|
+
status: 404,
|
|
252
|
+
data: { message: 'Not Found' },
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
(mockAxios.get as jest.Mock).mockRejectedValue(error);
|
|
256
|
+
|
|
257
|
+
await expect(apiService.listTodos()).rejects.toEqual(error);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle timeout errors', async () => {
|
|
261
|
+
const error = new Error('timeout');
|
|
262
|
+
(mockAxios.get as jest.Mock).mockRejectedValue(error);
|
|
263
|
+
|
|
264
|
+
await expect(apiService.listTodos()).rejects.toThrow('timeout');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Date Formatting', () => {
|
|
269
|
+
it('should convert YYYY-MM-DD to LocalDateTime format', async () => {
|
|
270
|
+
const mockResponse = { data: { id: 1 } };
|
|
271
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
272
|
+
|
|
273
|
+
await apiService.createTodo('test', '2026-04-15');
|
|
274
|
+
|
|
275
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
276
|
+
'/api/v1/todos',
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
dueDate: '2026-04-15T00:00:00',
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should convert YYYY-MM-DD HH:MM to LocalDateTime format', async () => {
|
|
284
|
+
const mockResponse = { data: { id: 1 } };
|
|
285
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
286
|
+
|
|
287
|
+
await apiService.createTodo('test', '2026-04-15 14:30');
|
|
288
|
+
|
|
289
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
290
|
+
'/api/v1/todos',
|
|
291
|
+
expect.objectContaining({
|
|
292
|
+
dueDate: '2026-04-15T14:30:00',
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should pass through ISO format dates', async () => {
|
|
298
|
+
const mockResponse = { data: { id: 1 } };
|
|
299
|
+
(mockAxios.post as jest.Mock).mockResolvedValue(mockResponse);
|
|
300
|
+
|
|
301
|
+
await apiService.createTodo('test', '2026-04-15T14:30:00');
|
|
302
|
+
|
|
303
|
+
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
304
|
+
'/api/v1/todos',
|
|
305
|
+
expect.objectContaining({
|
|
306
|
+
dueDate: '2026-04-15T14:30:00',
|
|
307
|
+
})
|
|
308
|
+
);
|
|
37
309
|
});
|
|
38
310
|
});
|
|
39
311
|
});
|