@nextsparkjs/core 0.1.0-beta.23 → 0.1.0-beta.25
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/dist/styles/classes.json +1 -1
- package/dist/styles/ui.css +1 -1
- package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/auth.cy.ts +199 -0
- package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +104 -0
- package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/tasks.cy.ts +274 -0
- package/dist/templates/contents/themes/starter/tests/cypress/support/e2e.ts +36 -4
- package/dist/templates/contents/themes/starter/tests/cypress.config.ts +18 -15
- package/dist/templates/contents/themes/starter/tests/jest/__mocks__/jose.js +22 -0
- package/dist/templates/contents/themes/starter/tests/jest/__mocks__/next-server.js +56 -0
- package/dist/templates/contents/themes/starter/tests/jest/example.test.ts +87 -0
- package/dist/templates/contents/themes/starter/tests/jest/jest.config.cjs +94 -0
- package/dist/templates/contents/themes/starter/tests/jest/services/tasks.service.test.ts +547 -0
- package/dist/templates/contents/themes/starter/tests/jest/setup.ts +170 -0
- package/package.json +12 -12
- package/scripts/build/docs-registry.mjs +0 -0
- package/scripts/create-theme.mjs +0 -0
- package/scripts/deploy/release-version.mjs +0 -0
- package/scripts/deploy/vercel-deploy.mjs +0 -0
- package/scripts/dev/watch-plugins.mjs +0 -0
- package/scripts/maintenance/update-core.mjs +0 -0
- package/scripts/setup/npm-postinstall.mjs +0 -0
- package/scripts/setup/setup-claude.mjs +0 -0
- package/scripts/validation/check-imports.sh +0 -0
- package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/auth.cy.ts +199 -0
- package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +104 -0
- package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/tasks.cy.ts +274 -0
- package/templates/contents/themes/starter/tests/jest/__mocks__/jose.js +22 -0
- package/templates/contents/themes/starter/tests/jest/__mocks__/next-server.js +56 -0
- package/templates/contents/themes/starter/tests/jest/example.test.ts +87 -0
- package/templates/contents/themes/starter/tests/jest/jest.config.cjs +94 -0
- package/templates/contents/themes/starter/tests/jest/services/tasks.service.test.ts +547 -0
- package/templates/contents/themes/starter/tests/jest/setup.ts +170 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +26 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - TasksService (Starter Theme)
|
|
3
|
+
*
|
|
4
|
+
* Tests for the TasksService class methods:
|
|
5
|
+
* - Input validation (required fields, empty strings)
|
|
6
|
+
* - Data transformation (database null → undefined)
|
|
7
|
+
* - Default values for optional fields
|
|
8
|
+
* - Error handling
|
|
9
|
+
*
|
|
10
|
+
* Focus on business logic WITHOUT actual database calls.
|
|
11
|
+
* Database functions are mocked to isolate the service logic.
|
|
12
|
+
*
|
|
13
|
+
* This is the starter theme version - it tests the read-only service methods.
|
|
14
|
+
* For full CRUD tests, see the default theme's tasks.service.test.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Mock the database module before importing the service
|
|
18
|
+
jest.mock('@nextsparkjs/core/lib/db', () => ({
|
|
19
|
+
queryOneWithRLS: jest.fn(),
|
|
20
|
+
queryWithRLS: jest.fn(),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
import { TasksService } from '@/contents/themes/starter/entities/tasks/tasks.service'
|
|
24
|
+
import type { Task, TaskStatus, TaskPriority } from '@/contents/themes/starter/entities/tasks/tasks.types'
|
|
25
|
+
|
|
26
|
+
// Get the mocked functions
|
|
27
|
+
const mockQueryOneWithRLS = jest.requireMock('@nextsparkjs/core/lib/db').queryOneWithRLS
|
|
28
|
+
const mockQueryWithRLS = jest.requireMock('@nextsparkjs/core/lib/db').queryWithRLS
|
|
29
|
+
|
|
30
|
+
// Helper to create a mock database task row
|
|
31
|
+
const createMockDbTask = (overrides = {}) => ({
|
|
32
|
+
id: 'task-123',
|
|
33
|
+
title: 'Test Task',
|
|
34
|
+
description: 'Test description',
|
|
35
|
+
status: 'todo' as TaskStatus,
|
|
36
|
+
priority: 'medium' as TaskPriority,
|
|
37
|
+
tags: ['tag1', 'tag2'],
|
|
38
|
+
dueDate: '2025-12-31',
|
|
39
|
+
estimatedHours: 5,
|
|
40
|
+
completed: false,
|
|
41
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
42
|
+
updatedAt: '2025-01-01T00:00:00Z',
|
|
43
|
+
...overrides,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('TasksService', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// getById
|
|
53
|
+
// ============================================================
|
|
54
|
+
describe('getById', () => {
|
|
55
|
+
describe('Input Validation', () => {
|
|
56
|
+
it('should throw error when id is empty', async () => {
|
|
57
|
+
await expect(TasksService.getById('', 'user-123'))
|
|
58
|
+
.rejects.toThrow('Task ID is required')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should throw error when id is whitespace only', async () => {
|
|
62
|
+
await expect(TasksService.getById(' ', 'user-123'))
|
|
63
|
+
.rejects.toThrow('Task ID is required')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should throw error when userId is empty', async () => {
|
|
67
|
+
await expect(TasksService.getById('task-123', ''))
|
|
68
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should throw error when userId is whitespace only', async () => {
|
|
72
|
+
await expect(TasksService.getById('task-123', ' '))
|
|
73
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('Successful Retrieval', () => {
|
|
78
|
+
it('should return task when found', async () => {
|
|
79
|
+
const mockTask = createMockDbTask()
|
|
80
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
81
|
+
|
|
82
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
83
|
+
|
|
84
|
+
expect(result).not.toBeNull()
|
|
85
|
+
expect(result?.id).toBe('task-123')
|
|
86
|
+
expect(result?.title).toBe('Test Task')
|
|
87
|
+
expect(mockQueryOneWithRLS).toHaveBeenCalledWith(
|
|
88
|
+
expect.stringContaining('SELECT'),
|
|
89
|
+
['task-123'],
|
|
90
|
+
'user-123'
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should return null when task not found', async () => {
|
|
95
|
+
mockQueryOneWithRLS.mockResolvedValue(null)
|
|
96
|
+
|
|
97
|
+
const result = await TasksService.getById('non-existent', 'user-123')
|
|
98
|
+
|
|
99
|
+
expect(result).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('Data Transformation', () => {
|
|
104
|
+
it('should transform null description to undefined', async () => {
|
|
105
|
+
const mockTask = createMockDbTask({ description: null })
|
|
106
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
107
|
+
|
|
108
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
109
|
+
|
|
110
|
+
expect(result?.description).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should transform null tags to undefined', async () => {
|
|
114
|
+
const mockTask = createMockDbTask({ tags: null })
|
|
115
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
116
|
+
|
|
117
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
118
|
+
|
|
119
|
+
expect(result?.tags).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should transform null dueDate to undefined', async () => {
|
|
123
|
+
const mockTask = createMockDbTask({ dueDate: null })
|
|
124
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
125
|
+
|
|
126
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
127
|
+
|
|
128
|
+
expect(result?.dueDate).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should transform null estimatedHours to undefined', async () => {
|
|
132
|
+
const mockTask = createMockDbTask({ estimatedHours: null })
|
|
133
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
134
|
+
|
|
135
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
136
|
+
|
|
137
|
+
expect(result?.estimatedHours).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should transform null completed to undefined', async () => {
|
|
141
|
+
const mockTask = createMockDbTask({ completed: null })
|
|
142
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
143
|
+
|
|
144
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
145
|
+
|
|
146
|
+
expect(result?.completed).toBeUndefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should preserve non-null values', async () => {
|
|
150
|
+
const mockTask = createMockDbTask()
|
|
151
|
+
mockQueryOneWithRLS.mockResolvedValue(mockTask)
|
|
152
|
+
|
|
153
|
+
const result = await TasksService.getById('task-123', 'user-123')
|
|
154
|
+
|
|
155
|
+
expect(result?.description).toBe('Test description')
|
|
156
|
+
expect(result?.tags).toEqual(['tag1', 'tag2'])
|
|
157
|
+
expect(result?.dueDate).toBe('2025-12-31')
|
|
158
|
+
expect(result?.estimatedHours).toBe(5)
|
|
159
|
+
expect(result?.completed).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('Error Handling', () => {
|
|
164
|
+
it('should wrap database errors with descriptive message', async () => {
|
|
165
|
+
mockQueryOneWithRLS.mockRejectedValue(new Error('Database connection failed'))
|
|
166
|
+
|
|
167
|
+
await expect(TasksService.getById('task-123', 'user-123'))
|
|
168
|
+
.rejects.toThrow('Database connection failed')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// list
|
|
175
|
+
// ============================================================
|
|
176
|
+
describe('list', () => {
|
|
177
|
+
describe('Input Validation', () => {
|
|
178
|
+
it('should throw error when userId is empty', async () => {
|
|
179
|
+
await expect(TasksService.list(''))
|
|
180
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should throw error when userId is whitespace only', async () => {
|
|
184
|
+
await expect(TasksService.list(' '))
|
|
185
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('Default Options', () => {
|
|
190
|
+
it('should use default limit of 10', async () => {
|
|
191
|
+
mockQueryWithRLS
|
|
192
|
+
.mockResolvedValueOnce([{ count: '5' }]) // count query
|
|
193
|
+
.mockResolvedValueOnce([]) // data query
|
|
194
|
+
|
|
195
|
+
await TasksService.list('user-123')
|
|
196
|
+
|
|
197
|
+
// Second call is the data query with LIMIT
|
|
198
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
199
|
+
expect(dataQueryCall[0]).toContain('LIMIT')
|
|
200
|
+
expect(dataQueryCall[1]).toContain(10) // default limit
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should use default offset of 0', async () => {
|
|
204
|
+
mockQueryWithRLS
|
|
205
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
206
|
+
.mockResolvedValueOnce([])
|
|
207
|
+
|
|
208
|
+
await TasksService.list('user-123')
|
|
209
|
+
|
|
210
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
211
|
+
expect(dataQueryCall[1]).toContain(0) // default offset
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should use default orderBy createdAt DESC', async () => {
|
|
215
|
+
mockQueryWithRLS
|
|
216
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
217
|
+
.mockResolvedValueOnce([])
|
|
218
|
+
|
|
219
|
+
await TasksService.list('user-123')
|
|
220
|
+
|
|
221
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
222
|
+
expect(dataQueryCall[0]).toContain('"createdAt"')
|
|
223
|
+
expect(dataQueryCall[0]).toContain('DESC')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('Filtering', () => {
|
|
228
|
+
it('should filter by status when provided', async () => {
|
|
229
|
+
mockQueryWithRLS
|
|
230
|
+
.mockResolvedValueOnce([{ count: '2' }])
|
|
231
|
+
.mockResolvedValueOnce([])
|
|
232
|
+
|
|
233
|
+
await TasksService.list('user-123', { status: 'todo' })
|
|
234
|
+
|
|
235
|
+
const countQueryCall = mockQueryWithRLS.mock.calls[0]
|
|
236
|
+
expect(countQueryCall[0]).toContain('status = $1')
|
|
237
|
+
expect(countQueryCall[1]).toContain('todo')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should filter by priority when provided', async () => {
|
|
241
|
+
mockQueryWithRLS
|
|
242
|
+
.mockResolvedValueOnce([{ count: '2' }])
|
|
243
|
+
.mockResolvedValueOnce([])
|
|
244
|
+
|
|
245
|
+
await TasksService.list('user-123', { priority: 'high' })
|
|
246
|
+
|
|
247
|
+
const countQueryCall = mockQueryWithRLS.mock.calls[0]
|
|
248
|
+
expect(countQueryCall[0]).toContain('priority = $1')
|
|
249
|
+
expect(countQueryCall[1]).toContain('high')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should combine multiple filters with AND', async () => {
|
|
253
|
+
mockQueryWithRLS
|
|
254
|
+
.mockResolvedValueOnce([{ count: '1' }])
|
|
255
|
+
.mockResolvedValueOnce([])
|
|
256
|
+
|
|
257
|
+
await TasksService.list('user-123', { status: 'todo', priority: 'high' })
|
|
258
|
+
|
|
259
|
+
const countQueryCall = mockQueryWithRLS.mock.calls[0]
|
|
260
|
+
expect(countQueryCall[0]).toContain('status = $1')
|
|
261
|
+
expect(countQueryCall[0]).toContain('priority = $2')
|
|
262
|
+
expect(countQueryCall[0]).toContain('AND')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('Ordering', () => {
|
|
267
|
+
const validOrderByFields = ['title', 'status', 'priority', 'dueDate', 'createdAt']
|
|
268
|
+
|
|
269
|
+
validOrderByFields.forEach(field => {
|
|
270
|
+
it(`should order by ${field} when specified`, async () => {
|
|
271
|
+
mockQueryWithRLS
|
|
272
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
273
|
+
.mockResolvedValueOnce([])
|
|
274
|
+
|
|
275
|
+
await TasksService.list('user-123', { orderBy: field as any })
|
|
276
|
+
|
|
277
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
278
|
+
// dueDate and createdAt need quotes in SQL
|
|
279
|
+
const expectedColumn = ['dueDate', 'createdAt'].includes(field)
|
|
280
|
+
? `"${field}"`
|
|
281
|
+
: field
|
|
282
|
+
expect(dataQueryCall[0]).toContain(expectedColumn)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should fallback to createdAt for invalid orderBy', async () => {
|
|
287
|
+
mockQueryWithRLS
|
|
288
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
289
|
+
.mockResolvedValueOnce([])
|
|
290
|
+
|
|
291
|
+
await TasksService.list('user-123', { orderBy: 'invalidField' as any })
|
|
292
|
+
|
|
293
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
294
|
+
expect(dataQueryCall[0]).toContain('"createdAt"')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should order ASC when orderDir is asc', async () => {
|
|
298
|
+
mockQueryWithRLS
|
|
299
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
300
|
+
.mockResolvedValueOnce([])
|
|
301
|
+
|
|
302
|
+
await TasksService.list('user-123', { orderDir: 'asc' })
|
|
303
|
+
|
|
304
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
305
|
+
expect(dataQueryCall[0]).toContain('ASC')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should order DESC when orderDir is desc', async () => {
|
|
309
|
+
mockQueryWithRLS
|
|
310
|
+
.mockResolvedValueOnce([{ count: '5' }])
|
|
311
|
+
.mockResolvedValueOnce([])
|
|
312
|
+
|
|
313
|
+
await TasksService.list('user-123', { orderDir: 'desc' })
|
|
314
|
+
|
|
315
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
316
|
+
expect(dataQueryCall[0]).toContain('DESC')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('Result Structure', () => {
|
|
321
|
+
it('should return tasks array and total count', async () => {
|
|
322
|
+
const mockTasks = [createMockDbTask(), createMockDbTask({ id: 'task-456' })]
|
|
323
|
+
mockQueryWithRLS
|
|
324
|
+
.mockResolvedValueOnce([{ count: '10' }])
|
|
325
|
+
.mockResolvedValueOnce(mockTasks)
|
|
326
|
+
|
|
327
|
+
const result = await TasksService.list('user-123')
|
|
328
|
+
|
|
329
|
+
expect(result.tasks).toHaveLength(2)
|
|
330
|
+
expect(result.total).toBe(10)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('should transform database rows to Task type', async () => {
|
|
334
|
+
const mockTask = createMockDbTask({ description: null, tags: null })
|
|
335
|
+
mockQueryWithRLS
|
|
336
|
+
.mockResolvedValueOnce([{ count: '1' }])
|
|
337
|
+
.mockResolvedValueOnce([mockTask])
|
|
338
|
+
|
|
339
|
+
const result = await TasksService.list('user-123')
|
|
340
|
+
|
|
341
|
+
expect(result.tasks[0].description).toBeUndefined()
|
|
342
|
+
expect(result.tasks[0].tags).toBeUndefined()
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should return empty array when no tasks found', async () => {
|
|
346
|
+
mockQueryWithRLS
|
|
347
|
+
.mockResolvedValueOnce([{ count: '0' }])
|
|
348
|
+
.mockResolvedValueOnce([])
|
|
349
|
+
|
|
350
|
+
const result = await TasksService.list('user-123')
|
|
351
|
+
|
|
352
|
+
expect(result.tasks).toEqual([])
|
|
353
|
+
expect(result.total).toBe(0)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// ============================================================
|
|
359
|
+
// getByStatus
|
|
360
|
+
// ============================================================
|
|
361
|
+
describe('getByStatus', () => {
|
|
362
|
+
it('should call list with status filter', async () => {
|
|
363
|
+
mockQueryWithRLS
|
|
364
|
+
.mockResolvedValueOnce([{ count: '2' }])
|
|
365
|
+
.mockResolvedValueOnce([createMockDbTask(), createMockDbTask({ id: 'task-456' })])
|
|
366
|
+
|
|
367
|
+
const result = await TasksService.getByStatus('user-123', 'in-progress')
|
|
368
|
+
|
|
369
|
+
expect(result).toHaveLength(2)
|
|
370
|
+
const countQueryCall = mockQueryWithRLS.mock.calls[0]
|
|
371
|
+
expect(countQueryCall[1]).toContain('in-progress')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should order by priority DESC', async () => {
|
|
375
|
+
mockQueryWithRLS
|
|
376
|
+
.mockResolvedValueOnce([{ count: '1' }])
|
|
377
|
+
.mockResolvedValueOnce([createMockDbTask()])
|
|
378
|
+
|
|
379
|
+
await TasksService.getByStatus('user-123', 'todo')
|
|
380
|
+
|
|
381
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
382
|
+
expect(dataQueryCall[0]).toContain('priority')
|
|
383
|
+
expect(dataQueryCall[0]).toContain('DESC')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('should use large limit to get all matching tasks', async () => {
|
|
387
|
+
mockQueryWithRLS
|
|
388
|
+
.mockResolvedValueOnce([{ count: '500' }])
|
|
389
|
+
.mockResolvedValueOnce([])
|
|
390
|
+
|
|
391
|
+
await TasksService.getByStatus('user-123', 'todo')
|
|
392
|
+
|
|
393
|
+
const dataQueryCall = mockQueryWithRLS.mock.calls[1]
|
|
394
|
+
expect(dataQueryCall[1]).toContain(1000) // Large limit
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
// ============================================================
|
|
399
|
+
// getOverdue
|
|
400
|
+
// ============================================================
|
|
401
|
+
describe('getOverdue', () => {
|
|
402
|
+
describe('Input Validation', () => {
|
|
403
|
+
it('should throw error when userId is empty', async () => {
|
|
404
|
+
await expect(TasksService.getOverdue(''))
|
|
405
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should throw error when userId is whitespace only', async () => {
|
|
409
|
+
await expect(TasksService.getOverdue(' '))
|
|
410
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
describe('Query', () => {
|
|
415
|
+
it('should query tasks with dueDate before today', async () => {
|
|
416
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
417
|
+
|
|
418
|
+
await TasksService.getOverdue('user-123')
|
|
419
|
+
|
|
420
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
421
|
+
expect(queryCall[0]).toContain('"dueDate" < CURRENT_DATE')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should exclude done tasks', async () => {
|
|
425
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
426
|
+
|
|
427
|
+
await TasksService.getOverdue('user-123')
|
|
428
|
+
|
|
429
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
430
|
+
expect(queryCall[0]).toContain("status != 'done'")
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('should exclude completed tasks', async () => {
|
|
434
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
435
|
+
|
|
436
|
+
await TasksService.getOverdue('user-123')
|
|
437
|
+
|
|
438
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
439
|
+
expect(queryCall[0]).toContain('completed IS NULL OR completed = false')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should order by dueDate ascending', async () => {
|
|
443
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
444
|
+
|
|
445
|
+
await TasksService.getOverdue('user-123')
|
|
446
|
+
|
|
447
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
448
|
+
expect(queryCall[0]).toContain('"dueDate" ASC')
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
describe('Result', () => {
|
|
453
|
+
it('should return array of overdue tasks', async () => {
|
|
454
|
+
const mockTasks = [
|
|
455
|
+
createMockDbTask({ dueDate: '2024-01-01' }),
|
|
456
|
+
createMockDbTask({ id: 'task-456', dueDate: '2024-06-01' }),
|
|
457
|
+
]
|
|
458
|
+
mockQueryWithRLS.mockResolvedValue(mockTasks)
|
|
459
|
+
|
|
460
|
+
const result = await TasksService.getOverdue('user-123')
|
|
461
|
+
|
|
462
|
+
expect(result).toHaveLength(2)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should transform database rows correctly', async () => {
|
|
466
|
+
const mockTask = createMockDbTask({ description: null })
|
|
467
|
+
mockQueryWithRLS.mockResolvedValue([mockTask])
|
|
468
|
+
|
|
469
|
+
const result = await TasksService.getOverdue('user-123')
|
|
470
|
+
|
|
471
|
+
expect(result[0].description).toBeUndefined()
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
// ============================================================
|
|
477
|
+
// getDueToday
|
|
478
|
+
// ============================================================
|
|
479
|
+
describe('getDueToday', () => {
|
|
480
|
+
describe('Input Validation', () => {
|
|
481
|
+
it('should throw error when userId is empty', async () => {
|
|
482
|
+
await expect(TasksService.getDueToday(''))
|
|
483
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('should throw error when userId is whitespace only', async () => {
|
|
487
|
+
await expect(TasksService.getDueToday(' '))
|
|
488
|
+
.rejects.toThrow('User ID is required for authentication')
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('Query', () => {
|
|
493
|
+
it('should query tasks with dueDate equal to today', async () => {
|
|
494
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
495
|
+
|
|
496
|
+
await TasksService.getDueToday('user-123')
|
|
497
|
+
|
|
498
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
499
|
+
expect(queryCall[0]).toContain('"dueDate" = CURRENT_DATE')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('should exclude done tasks', async () => {
|
|
503
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
504
|
+
|
|
505
|
+
await TasksService.getDueToday('user-123')
|
|
506
|
+
|
|
507
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
508
|
+
expect(queryCall[0]).toContain("status != 'done'")
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('should order by priority DESC then createdAt ASC', async () => {
|
|
512
|
+
mockQueryWithRLS.mockResolvedValue([])
|
|
513
|
+
|
|
514
|
+
await TasksService.getDueToday('user-123')
|
|
515
|
+
|
|
516
|
+
const queryCall = mockQueryWithRLS.mock.calls[0]
|
|
517
|
+
expect(queryCall[0]).toContain('priority DESC')
|
|
518
|
+
expect(queryCall[0]).toContain('"createdAt" ASC')
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('Result', () => {
|
|
523
|
+
it('should return array of tasks due today', async () => {
|
|
524
|
+
const today = new Date().toISOString().split('T')[0]
|
|
525
|
+
const mockTasks = [
|
|
526
|
+
createMockDbTask({ dueDate: today }),
|
|
527
|
+
createMockDbTask({ id: 'task-456', dueDate: today }),
|
|
528
|
+
]
|
|
529
|
+
mockQueryWithRLS.mockResolvedValue(mockTasks)
|
|
530
|
+
|
|
531
|
+
const result = await TasksService.getDueToday('user-123')
|
|
532
|
+
|
|
533
|
+
expect(result).toHaveLength(2)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('should transform database rows correctly', async () => {
|
|
537
|
+
const mockTask = createMockDbTask({ description: null, tags: null })
|
|
538
|
+
mockQueryWithRLS.mockResolvedValue([mockTask])
|
|
539
|
+
|
|
540
|
+
const result = await TasksService.getDueToday('user-123')
|
|
541
|
+
|
|
542
|
+
expect(result[0].description).toBeUndefined()
|
|
543
|
+
expect(result[0].tags).toBeUndefined()
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
})
|