@tellescope/sdk 1.245.1 → 1.246.2
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/lib/cjs/tests/api_tests/account_switcher.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/account_switcher.test.js +445 -0
- package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js +357 -0
- package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js +163 -0
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js +483 -0
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/email_utils.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/email_utils.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/email_utils.test.js +141 -0
- package/lib/cjs/tests/api_tests/email_utils.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js +268 -0
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts +20 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js +692 -20
- package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +157 -122
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/tests/api_tests/account_switcher.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/account_switcher.test.js +438 -0
- package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -0
- package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js +353 -0
- package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_limits.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js +163 -0
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js +479 -0
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -0
- package/lib/esm/tests/api_tests/email_utils.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/email_utils.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/email_utils.test.js +137 -0
- package/lib/esm/tests/api_tests/email_utils.test.js.map +1 -0
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js +264 -0
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js.map +1 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts +20 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/time_tracks.test.js +687 -20
- package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +158 -123
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/beluga_pharmacy_mappings.test.ts +351 -0
- package/src/tests/api_tests/calendar_event_limits.test.ts +195 -0
- package/src/tests/api_tests/time_tracks.test.ts +542 -16
- package/src/tests/tests.ts +34 -5
- package/test_generated.pdf +0 -0
|
@@ -8,6 +8,18 @@ const log = console.log
|
|
|
8
8
|
|
|
9
9
|
const host = process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
|
|
10
10
|
|
|
11
|
+
// Helper to assert that an async function throws an error
|
|
12
|
+
const assert_throws = async (fn: () => Promise<any>, description: string) => {
|
|
13
|
+
try {
|
|
14
|
+
await fn()
|
|
15
|
+
assert(false, `${description} - expected error but succeeded`)
|
|
16
|
+
} catch (e: any) {
|
|
17
|
+
// SDK parseError returns the response body { message, info } for 4xx errors
|
|
18
|
+
assert(e?.code === 400 || e?.statusCode === 400 || typeof e?.message === 'string',
|
|
19
|
+
`${description} - expected error, got: ${JSON.stringify(e)}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
// Unit tests for calculateTimeTrackDuration
|
|
12
24
|
const test_calculateTimeTrackDuration = () => {
|
|
13
25
|
log_header("calculateTimeTrackDuration Unit Tests")
|
|
@@ -66,7 +78,7 @@ const test_calculateTimeTrackDuration = () => {
|
|
|
66
78
|
])
|
|
67
79
|
assert(test7 === 2700000, `Three intervals (10+30+5 min) should be 2700000 ms, got ${test7}`)
|
|
68
80
|
|
|
69
|
-
log("
|
|
81
|
+
log("All calculateTimeTrackDuration unit tests passed")
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
// API tests for time_tracks CRUD operations
|
|
@@ -92,21 +104,21 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
92
104
|
assert(timeTrack.timestamps?.[0].type === 'start', `First timestamp should be 'start', got ${timeTrack.timestamps?.[0].type}`)
|
|
93
105
|
assert(!timeTrack.closedAt, "closedAt should not be set initially")
|
|
94
106
|
assert(!timeTrack.totalDurationInMS, "totalDurationInMS should not be set initially")
|
|
95
|
-
log("
|
|
107
|
+
log("Time track created with auto-set userId and initial timestamp")
|
|
96
108
|
|
|
97
109
|
// Test 2: Read the time track
|
|
98
110
|
log("Reading time track...")
|
|
99
111
|
const fetchedTimeTrack = await sdk.api.time_tracks.getOne(timeTrack.id)
|
|
100
112
|
assert(fetchedTimeTrack.id === timeTrack.id, "Fetched time track should have same id")
|
|
101
113
|
assert(fetchedTimeTrack.title === "Test Time Track", "Fetched title should match")
|
|
102
|
-
log("
|
|
114
|
+
log("Time track retrieved successfully")
|
|
103
115
|
|
|
104
116
|
// Test 3: Get all time tracks for current user
|
|
105
117
|
log("Getting all time tracks for current user...")
|
|
106
118
|
const allTimeTracks = await sdk.api.time_tracks.getSome({ filter: { userId } })
|
|
107
119
|
assert(allTimeTracks.length >= 1, `Should have at least 1 time track, got ${allTimeTracks.length}`)
|
|
108
120
|
assert(!!allTimeTracks.find(t => t.id === timeTrack.id), "Should find our created time track")
|
|
109
|
-
log("
|
|
121
|
+
log("Retrieved all time tracks for user")
|
|
110
122
|
|
|
111
123
|
// Test 4: Create enduser and link time track
|
|
112
124
|
log("Creating enduser for time track linkage...")
|
|
@@ -116,7 +128,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
116
128
|
lname: "Track",
|
|
117
129
|
})
|
|
118
130
|
enduserId = enduser.id
|
|
119
|
-
log("
|
|
131
|
+
log("Enduser created")
|
|
120
132
|
|
|
121
133
|
// Test 5: Create time track with enduserId
|
|
122
134
|
log("Creating time track linked to enduser...")
|
|
@@ -125,7 +137,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
125
137
|
enduserId: enduser.id,
|
|
126
138
|
} as any)
|
|
127
139
|
assert(linkedTimeTrack.enduserId === enduser.id, `enduserId should be set to ${enduser.id}, got ${linkedTimeTrack.enduserId}`)
|
|
128
|
-
log("
|
|
140
|
+
log("Time track linked to enduser")
|
|
129
141
|
|
|
130
142
|
// Test 6: Update time track - add pause timestamp
|
|
131
143
|
log("Adding pause timestamp...")
|
|
@@ -138,7 +150,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
138
150
|
}, { replaceObjectFields: true })
|
|
139
151
|
assert(pausedTimeTrack.timestamps?.length === 2, `Should have 2 timestamps after pause, got ${pausedTimeTrack.timestamps?.length}`)
|
|
140
152
|
assert(pausedTimeTrack.timestamps?.[1].type === 'pause', `Second timestamp should be 'pause', got ${pausedTimeTrack.timestamps?.[1].type}`)
|
|
141
|
-
log("
|
|
153
|
+
log("Pause timestamp added")
|
|
142
154
|
|
|
143
155
|
// Test 7: Update time track - add resume timestamp
|
|
144
156
|
log("Adding resume timestamp...")
|
|
@@ -151,7 +163,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
151
163
|
}, { replaceObjectFields: true })
|
|
152
164
|
assert(resumedTimeTrack.timestamps?.length === 3, `Should have 3 timestamps after resume, got ${resumedTimeTrack.timestamps?.length}`)
|
|
153
165
|
assert(resumedTimeTrack.timestamps?.[2].type === 'resume', `Third timestamp should be 'resume', got ${resumedTimeTrack.timestamps?.[2].type}`)
|
|
154
|
-
log("
|
|
166
|
+
log("Resume timestamp added")
|
|
155
167
|
|
|
156
168
|
// Test 8: Close time track and verify auto-calculation
|
|
157
169
|
log("Closing time track with closedAt...")
|
|
@@ -168,7 +180,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
168
180
|
assert(!!finalTimeTrack.closedAt, "closedAt should be set")
|
|
169
181
|
assert(typeof finalTimeTrack.totalDurationInMS === 'number', `totalDurationInMS should be a number, got ${typeof finalTimeTrack.totalDurationInMS}`)
|
|
170
182
|
assert(finalTimeTrack.totalDurationInMS! > 0, `totalDurationInMS should be > 0, got ${finalTimeTrack.totalDurationInMS}`)
|
|
171
|
-
log(
|
|
183
|
+
log(`Time track closed with auto-calculated duration: ${finalTimeTrack.totalDurationInMS} ms`)
|
|
172
184
|
|
|
173
185
|
// Test 9: Update title
|
|
174
186
|
log("Updating time track title...")
|
|
@@ -176,7 +188,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
176
188
|
title: "Updated Time Track Title",
|
|
177
189
|
})
|
|
178
190
|
assert(updatedTimeTrack.title === "Updated Time Track Title", "Title should be updated")
|
|
179
|
-
log("
|
|
191
|
+
log("Time track title updated")
|
|
180
192
|
|
|
181
193
|
// Test 10: Filter by closedAt (get active time tracks)
|
|
182
194
|
log("Filtering for active time tracks (no closedAt)...")
|
|
@@ -186,14 +198,14 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
186
198
|
const activeTimeTracks = allUserTimeTracks.filter(t => !t.closedAt)
|
|
187
199
|
assert(!activeTimeTracks.find(t => t.id === timeTrack.id), "Closed time track should not appear in active filter")
|
|
188
200
|
assert(!!activeTimeTracks.find(t => t.id === linkedTimeTrack.id), "Unclosed time track should appear in active filter")
|
|
189
|
-
log("
|
|
201
|
+
log("Active time tracks filtered correctly")
|
|
190
202
|
|
|
191
203
|
// Test 11: Access control - non-admin user should only see their own time tracks
|
|
192
204
|
log("Testing access control with non-admin user...")
|
|
193
205
|
const nonAdminTimeTracks = await sdkNonAdmin.api.time_tracks.getSome({})
|
|
194
206
|
const hasOtherUserTimeTrack = nonAdminTimeTracks.find(t => t.userId !== sdkNonAdmin.userInfo.id)
|
|
195
207
|
assert(!hasOtherUserTimeTrack, "Non-admin user should not see other users' time tracks")
|
|
196
|
-
log("
|
|
208
|
+
log("Access control working correctly")
|
|
197
209
|
|
|
198
210
|
// Test 12: Delete time tracks
|
|
199
211
|
log("Deleting time tracks...")
|
|
@@ -203,9 +215,9 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
203
215
|
const deletedCheck = await sdk.api.time_tracks.getSome({})
|
|
204
216
|
const stillExists = deletedCheck.filter(t => t.id === timeTrack.id || t.id === linkedTimeTrack.id)
|
|
205
217
|
assert(stillExists.length === 0, "Time tracks should be deleted")
|
|
206
|
-
log("
|
|
218
|
+
log("Time tracks deleted successfully")
|
|
207
219
|
|
|
208
|
-
log("
|
|
220
|
+
log("All time tracks API tests passed!")
|
|
209
221
|
|
|
210
222
|
} finally {
|
|
211
223
|
// Cleanup
|
|
@@ -226,6 +238,515 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
|
|
|
226
238
|
}
|
|
227
239
|
}
|
|
228
240
|
|
|
241
|
+
// ============================================================
|
|
242
|
+
// Group A: Historical Time Track Creation
|
|
243
|
+
// ============================================================
|
|
244
|
+
export const time_tracks_historical_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
245
|
+
log_header("Time Tracks - Historical Creation Tests")
|
|
246
|
+
|
|
247
|
+
const trackIds: string[] = []
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const now = new Date()
|
|
251
|
+
const oneHourAgo = new Date(now.getTime() - 3600000)
|
|
252
|
+
const twoHoursAgo = new Date(now.getTime() - 7200000)
|
|
253
|
+
|
|
254
|
+
// Test A1: Create historical time track with all required fields
|
|
255
|
+
log("A1: Creating historical time track with all required fields...")
|
|
256
|
+
const historical = await sdk.api.time_tracks.createOne({
|
|
257
|
+
title: "Historical Entry",
|
|
258
|
+
isHistorical: true,
|
|
259
|
+
closedAt: now,
|
|
260
|
+
lockedAt: now,
|
|
261
|
+
lockedByUserId: sdk.userInfo.id,
|
|
262
|
+
totalDurationInMS: 3600000,
|
|
263
|
+
timestamps: [
|
|
264
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
265
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
266
|
+
],
|
|
267
|
+
} as any)
|
|
268
|
+
trackIds.push(historical.id)
|
|
269
|
+
|
|
270
|
+
assert(historical.isHistorical === true, "isHistorical should be true")
|
|
271
|
+
assert(!!historical.closedAt, "closedAt should be set")
|
|
272
|
+
assert(!!historical.lockedAt, "lockedAt should be set")
|
|
273
|
+
assert(historical.lockedByUserId === sdk.userInfo.id, "lockedByUserId should match")
|
|
274
|
+
assert(historical.totalDurationInMS === 3600000, `totalDurationInMS should be 3600000, got ${historical.totalDurationInMS}`)
|
|
275
|
+
assert(historical.timestamps?.length === 2, "timestamps should have 2 entries")
|
|
276
|
+
log("A1: Historical time track created with all fields persisted")
|
|
277
|
+
|
|
278
|
+
// Test A2: Create historical without closedAt - expect 400
|
|
279
|
+
log("A2: Creating historical without closedAt (should fail)...")
|
|
280
|
+
await assert_throws(
|
|
281
|
+
() => sdk.api.time_tracks.createOne({
|
|
282
|
+
title: "Missing ClosedAt",
|
|
283
|
+
isHistorical: true,
|
|
284
|
+
lockedAt: now,
|
|
285
|
+
lockedByUserId: sdk.userInfo.id,
|
|
286
|
+
totalDurationInMS: 3600000,
|
|
287
|
+
timestamps: [{ type: 'start', timestamp: twoHoursAgo }],
|
|
288
|
+
} as any),
|
|
289
|
+
"A2: Historical without closedAt"
|
|
290
|
+
)
|
|
291
|
+
log("A2: Correctly rejected historical without closedAt")
|
|
292
|
+
|
|
293
|
+
// Test A3: Create historical without lockedAt - expect 400
|
|
294
|
+
log("A3: Creating historical without lockedAt (should fail)...")
|
|
295
|
+
await assert_throws(
|
|
296
|
+
() => sdk.api.time_tracks.createOne({
|
|
297
|
+
title: "Missing LockedAt",
|
|
298
|
+
isHistorical: true,
|
|
299
|
+
closedAt: now,
|
|
300
|
+
lockedByUserId: sdk.userInfo.id,
|
|
301
|
+
totalDurationInMS: 3600000,
|
|
302
|
+
timestamps: [{ type: 'start', timestamp: twoHoursAgo }],
|
|
303
|
+
} as any),
|
|
304
|
+
"A3: Historical without lockedAt"
|
|
305
|
+
)
|
|
306
|
+
log("A3: Correctly rejected historical without lockedAt")
|
|
307
|
+
|
|
308
|
+
// Test A4: Verify isHistorical cannot be updated (updatesDisabled)
|
|
309
|
+
log("A4: Attempting to update isHistorical (should be rejected)...")
|
|
310
|
+
await assert_throws(
|
|
311
|
+
() => sdk.api.time_tracks.updateOne(historical.id, {
|
|
312
|
+
isHistorical: false,
|
|
313
|
+
} as any),
|
|
314
|
+
"A4: isHistorical update should be rejected"
|
|
315
|
+
)
|
|
316
|
+
// Confirm it's still true
|
|
317
|
+
const fetched = await sdk.api.time_tracks.getOne(historical.id)
|
|
318
|
+
assert(fetched.isHistorical === true, "isHistorical should remain true after rejected update")
|
|
319
|
+
log("A4: isHistorical correctly rejected on update")
|
|
320
|
+
|
|
321
|
+
log("All historical creation tests passed!")
|
|
322
|
+
|
|
323
|
+
} finally {
|
|
324
|
+
for (const id of trackIds) {
|
|
325
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================
|
|
331
|
+
// Group B: Correction Flow
|
|
332
|
+
// ============================================================
|
|
333
|
+
export const time_tracks_correction_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
334
|
+
log_header("Time Tracks - Correction Flow Tests")
|
|
335
|
+
|
|
336
|
+
const trackIds: string[] = []
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const now = new Date()
|
|
340
|
+
|
|
341
|
+
// Create a real-time track and close it
|
|
342
|
+
log("B0: Creating and closing real-time track...")
|
|
343
|
+
const track = await sdk.api.time_tracks.createOne({
|
|
344
|
+
title: "Track for Correction",
|
|
345
|
+
} as any)
|
|
346
|
+
trackIds.push(track.id)
|
|
347
|
+
|
|
348
|
+
// Add pause
|
|
349
|
+
await sdk.api.time_tracks.updateOne(track.id, {
|
|
350
|
+
timestamps: [
|
|
351
|
+
...(track.timestamps || []),
|
|
352
|
+
{ type: 'pause', timestamp: new Date() },
|
|
353
|
+
]
|
|
354
|
+
}, { replaceObjectFields: true })
|
|
355
|
+
|
|
356
|
+
// Close it
|
|
357
|
+
const closedAt = new Date()
|
|
358
|
+
await sdk.api.time_tracks.updateOne(track.id, { closedAt })
|
|
359
|
+
|
|
360
|
+
// Wait for auto-calculated duration
|
|
361
|
+
await wait(undefined, 1500)
|
|
362
|
+
|
|
363
|
+
const closed = await sdk.api.time_tracks.getOne(track.id)
|
|
364
|
+
assert(!!closed.closedAt, "Track should be closed")
|
|
365
|
+
assert(typeof closed.totalDurationInMS === 'number', "Should have auto-calculated totalDurationInMS")
|
|
366
|
+
const originalDuration = closed.totalDurationInMS!
|
|
367
|
+
log(`B0: Track closed with auto-calculated duration: ${originalDuration} ms`)
|
|
368
|
+
|
|
369
|
+
// Test B1: Apply correction with all required fields
|
|
370
|
+
log("B1: Applying correction with all required fields...")
|
|
371
|
+
const correctionTime = new Date()
|
|
372
|
+
const correctedTrack = await sdk.api.time_tracks.updateOne(track.id, {
|
|
373
|
+
correctedAt: correctionTime,
|
|
374
|
+
correctedByUserId: sdk.userInfo.id,
|
|
375
|
+
correctionNote: "Forgot to pause during lunch",
|
|
376
|
+
originalTotalDurationInMS: originalDuration,
|
|
377
|
+
totalDurationInMS: originalDuration - 1800000, // Subtract 30 min
|
|
378
|
+
lockedAt: correctionTime,
|
|
379
|
+
lockedByUserId: sdk.userInfo.id,
|
|
380
|
+
} as any)
|
|
381
|
+
assert(!!correctedTrack.correctedAt, "correctedAt should be set")
|
|
382
|
+
assert(correctedTrack.correctedByUserId === sdk.userInfo.id, "correctedByUserId should match")
|
|
383
|
+
assert(correctedTrack.correctionNote === "Forgot to pause during lunch", "correctionNote should match")
|
|
384
|
+
assert(correctedTrack.originalTotalDurationInMS === originalDuration, "originalTotalDurationInMS should preserve old value")
|
|
385
|
+
assert(correctedTrack.totalDurationInMS === originalDuration - 1800000, "totalDurationInMS should be corrected value")
|
|
386
|
+
assert(!!correctedTrack.lockedAt, "lockedAt should be set")
|
|
387
|
+
log("B1: Correction applied successfully with all fields persisted")
|
|
388
|
+
|
|
389
|
+
// Test B2: Correction without originalTotalDurationInMS - expect 400
|
|
390
|
+
log("B2: Correction without originalTotalDurationInMS (should fail)...")
|
|
391
|
+
const track2 = await sdk.api.time_tracks.createOne({ title: "Track for B2" } as any)
|
|
392
|
+
trackIds.push(track2.id)
|
|
393
|
+
await sdk.api.time_tracks.updateOne(track2.id, { closedAt: new Date() })
|
|
394
|
+
await wait(undefined, 1000)
|
|
395
|
+
const closed2 = await sdk.api.time_tracks.getOne(track2.id)
|
|
396
|
+
await assert_throws(
|
|
397
|
+
() => sdk.api.time_tracks.updateOne(track2.id, {
|
|
398
|
+
correctedAt: new Date(),
|
|
399
|
+
totalDurationInMS: 1000,
|
|
400
|
+
lockedAt: new Date(),
|
|
401
|
+
lockedByUserId: sdk.userInfo.id,
|
|
402
|
+
} as any),
|
|
403
|
+
"B2: Correction without originalTotalDurationInMS"
|
|
404
|
+
)
|
|
405
|
+
log("B2: Correctly rejected correction without originalTotalDurationInMS")
|
|
406
|
+
|
|
407
|
+
// Test B3: Correction without lockedAt - expect 400
|
|
408
|
+
log("B3: Correction without lockedAt (should fail)...")
|
|
409
|
+
await assert_throws(
|
|
410
|
+
() => sdk.api.time_tracks.updateOne(track2.id, {
|
|
411
|
+
correctedAt: new Date(),
|
|
412
|
+
originalTotalDurationInMS: closed2.totalDurationInMS || 0,
|
|
413
|
+
totalDurationInMS: 1000,
|
|
414
|
+
lockedByUserId: sdk.userInfo.id,
|
|
415
|
+
} as any),
|
|
416
|
+
"B3: Correction without lockedAt"
|
|
417
|
+
)
|
|
418
|
+
log("B3: Correctly rejected correction without lockedAt")
|
|
419
|
+
|
|
420
|
+
// Test B4: After lock, attempt to update title - expect 400
|
|
421
|
+
log("B4: Updating title on locked track (should fail)...")
|
|
422
|
+
await assert_throws(
|
|
423
|
+
() => sdk.api.time_tracks.updateOne(track.id, {
|
|
424
|
+
title: "Should Not Work",
|
|
425
|
+
} as any),
|
|
426
|
+
"B4: Title update on locked track"
|
|
427
|
+
)
|
|
428
|
+
log("B4: Correctly rejected title update on locked track")
|
|
429
|
+
|
|
430
|
+
// Test B5: After lock, attempt second correction - expect 400
|
|
431
|
+
log("B5: Second correction on locked track (should fail)...")
|
|
432
|
+
await assert_throws(
|
|
433
|
+
() => sdk.api.time_tracks.updateOne(track.id, {
|
|
434
|
+
correctedAt: new Date(),
|
|
435
|
+
originalTotalDurationInMS: correctedTrack.totalDurationInMS,
|
|
436
|
+
totalDurationInMS: 500,
|
|
437
|
+
lockedAt: new Date(),
|
|
438
|
+
lockedByUserId: sdk.userInfo.id,
|
|
439
|
+
} as any),
|
|
440
|
+
"B5: Second correction on locked track"
|
|
441
|
+
)
|
|
442
|
+
log("B5: Correctly rejected second correction on locked track")
|
|
443
|
+
|
|
444
|
+
log("All correction flow tests passed!")
|
|
445
|
+
|
|
446
|
+
} finally {
|
|
447
|
+
for (const id of trackIds) {
|
|
448
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================
|
|
454
|
+
// Group C: Review Flow
|
|
455
|
+
// ============================================================
|
|
456
|
+
export const time_tracks_review_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
457
|
+
log_header("Time Tracks - Review Flow Tests")
|
|
458
|
+
|
|
459
|
+
const trackIds: string[] = []
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const now = new Date()
|
|
463
|
+
const oneHourAgo = new Date(now.getTime() - 3600000)
|
|
464
|
+
const twoHoursAgo = new Date(now.getTime() - 7200000)
|
|
465
|
+
|
|
466
|
+
// Create a historical track for review testing
|
|
467
|
+
log("C0: Creating historical track for review tests...")
|
|
468
|
+
const historical = await sdk.api.time_tracks.createOne({
|
|
469
|
+
title: "Track for Review",
|
|
470
|
+
isHistorical: true,
|
|
471
|
+
closedAt: now,
|
|
472
|
+
lockedAt: now,
|
|
473
|
+
lockedByUserId: sdk.userInfo.id,
|
|
474
|
+
totalDurationInMS: 3600000,
|
|
475
|
+
timestamps: [
|
|
476
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
477
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
478
|
+
],
|
|
479
|
+
} as any)
|
|
480
|
+
trackIds.push(historical.id)
|
|
481
|
+
log("C0: Historical track created")
|
|
482
|
+
|
|
483
|
+
// Test C1: Review by different user (approval)
|
|
484
|
+
log("C1: Review by different user (approval)...")
|
|
485
|
+
const reviewTime = new Date()
|
|
486
|
+
const reviewed = await sdk.api.time_tracks.updateOne(historical.id, {
|
|
487
|
+
reviewedAt: reviewTime,
|
|
488
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
489
|
+
reviewApproved: true,
|
|
490
|
+
} as any)
|
|
491
|
+
assert(!!reviewed.reviewedAt, "reviewedAt should be set")
|
|
492
|
+
assert(reviewed.reviewedByUserId === sdkNonAdmin.userInfo.id, "reviewedByUserId should match non-admin")
|
|
493
|
+
assert(reviewed.reviewApproved === true, "reviewApproved should be true")
|
|
494
|
+
log("C1: Review approved successfully by different user")
|
|
495
|
+
|
|
496
|
+
// Test C2: Self-review (owner reviews own track) - expect 400
|
|
497
|
+
log("C2: Self-review (should fail)...")
|
|
498
|
+
const historical2 = await sdk.api.time_tracks.createOne({
|
|
499
|
+
title: "Track for Self-Review",
|
|
500
|
+
isHistorical: true,
|
|
501
|
+
closedAt: now,
|
|
502
|
+
lockedAt: now,
|
|
503
|
+
lockedByUserId: sdk.userInfo.id,
|
|
504
|
+
totalDurationInMS: 3600000,
|
|
505
|
+
timestamps: [
|
|
506
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
507
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
508
|
+
],
|
|
509
|
+
} as any)
|
|
510
|
+
trackIds.push(historical2.id)
|
|
511
|
+
|
|
512
|
+
await assert_throws(
|
|
513
|
+
() => sdk.api.time_tracks.updateOne(historical2.id, {
|
|
514
|
+
reviewedAt: new Date(),
|
|
515
|
+
reviewedByUserId: sdk.userInfo.id, // same as track owner
|
|
516
|
+
reviewApproved: true,
|
|
517
|
+
} as any),
|
|
518
|
+
"C2: Self-review"
|
|
519
|
+
)
|
|
520
|
+
log("C2: Correctly rejected self-review")
|
|
521
|
+
|
|
522
|
+
// Test C3: Review fields updatable even after lock
|
|
523
|
+
log("C3: Review fields updatable after lock...")
|
|
524
|
+
const updatedReview = await sdk.api.time_tracks.updateOne(historical.id, {
|
|
525
|
+
reviewedAt: new Date(),
|
|
526
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
527
|
+
reviewApproved: false,
|
|
528
|
+
reviewNote: "Hours seem too high, please double-check",
|
|
529
|
+
} as any)
|
|
530
|
+
assert(updatedReview.reviewApproved === false, "reviewApproved should be updated to false")
|
|
531
|
+
assert(updatedReview.reviewNote === "Hours seem too high, please double-check", "reviewNote should be set")
|
|
532
|
+
log("C3: Review fields correctly updatable on locked track")
|
|
533
|
+
|
|
534
|
+
// Test C4: Rejection flow with reviewNote
|
|
535
|
+
log("C4: Rejection flow with reviewNote...")
|
|
536
|
+
const historical3 = await sdk.api.time_tracks.createOne({
|
|
537
|
+
title: "Track for Rejection",
|
|
538
|
+
isHistorical: true,
|
|
539
|
+
closedAt: now,
|
|
540
|
+
lockedAt: now,
|
|
541
|
+
lockedByUserId: sdk.userInfo.id,
|
|
542
|
+
totalDurationInMS: 1800000,
|
|
543
|
+
timestamps: [
|
|
544
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
545
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
546
|
+
],
|
|
547
|
+
} as any)
|
|
548
|
+
trackIds.push(historical3.id)
|
|
549
|
+
|
|
550
|
+
const rejected = await sdk.api.time_tracks.updateOne(historical3.id, {
|
|
551
|
+
reviewedAt: new Date(),
|
|
552
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
553
|
+
reviewApproved: false,
|
|
554
|
+
reviewNote: "Rejected - timestamps don't match claimed duration",
|
|
555
|
+
} as any)
|
|
556
|
+
assert(rejected.reviewApproved === false, "reviewApproved should be false")
|
|
557
|
+
assert(rejected.reviewNote === "Rejected - timestamps don't match claimed duration", "reviewNote should match")
|
|
558
|
+
log("C4: Rejection flow completed successfully")
|
|
559
|
+
|
|
560
|
+
log("All review flow tests passed!")
|
|
561
|
+
|
|
562
|
+
} finally {
|
|
563
|
+
for (const id of trackIds) {
|
|
564
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ============================================================
|
|
570
|
+
// Group D: Lock Enforcement
|
|
571
|
+
// ============================================================
|
|
572
|
+
export const time_tracks_lock_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
573
|
+
log_header("Time Tracks - Lock Enforcement Tests")
|
|
574
|
+
|
|
575
|
+
const trackIds: string[] = []
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const now = new Date()
|
|
579
|
+
const oneHourAgo = new Date(now.getTime() - 3600000)
|
|
580
|
+
const twoHoursAgo = new Date(now.getTime() - 7200000)
|
|
581
|
+
|
|
582
|
+
// Create a locked historical track
|
|
583
|
+
log("D0: Creating locked historical track...")
|
|
584
|
+
const locked = await sdk.api.time_tracks.createOne({
|
|
585
|
+
title: "Locked Track",
|
|
586
|
+
isHistorical: true,
|
|
587
|
+
closedAt: now,
|
|
588
|
+
lockedAt: now,
|
|
589
|
+
lockedByUserId: sdk.userInfo.id,
|
|
590
|
+
totalDurationInMS: 3600000,
|
|
591
|
+
timestamps: [
|
|
592
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
593
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
594
|
+
],
|
|
595
|
+
} as any)
|
|
596
|
+
trackIds.push(locked.id)
|
|
597
|
+
log("D0: Locked track created")
|
|
598
|
+
|
|
599
|
+
// Test D1: Locked track rejects updates to timestamps
|
|
600
|
+
log("D1: Updating timestamps on locked track (should fail)...")
|
|
601
|
+
await assert_throws(
|
|
602
|
+
() => sdk.api.time_tracks.updateOne(locked.id, {
|
|
603
|
+
timestamps: [{ type: 'start', timestamp: new Date() }],
|
|
604
|
+
} as any),
|
|
605
|
+
"D1: timestamps update on locked track"
|
|
606
|
+
)
|
|
607
|
+
log("D1: Correctly rejected timestamps update")
|
|
608
|
+
|
|
609
|
+
// Test D2: Locked track rejects updates to closedAt
|
|
610
|
+
log("D2: Updating closedAt on locked track (should fail)...")
|
|
611
|
+
await assert_throws(
|
|
612
|
+
() => sdk.api.time_tracks.updateOne(locked.id, {
|
|
613
|
+
closedAt: new Date(),
|
|
614
|
+
} as any),
|
|
615
|
+
"D2: closedAt update on locked track"
|
|
616
|
+
)
|
|
617
|
+
log("D2: Correctly rejected closedAt update")
|
|
618
|
+
|
|
619
|
+
// Test D3: Locked track rejects updates to totalDurationInMS
|
|
620
|
+
log("D3: Updating totalDurationInMS on locked track (should fail)...")
|
|
621
|
+
await assert_throws(
|
|
622
|
+
() => sdk.api.time_tracks.updateOne(locked.id, {
|
|
623
|
+
totalDurationInMS: 999,
|
|
624
|
+
} as any),
|
|
625
|
+
"D3: totalDurationInMS update on locked track"
|
|
626
|
+
)
|
|
627
|
+
log("D3: Correctly rejected totalDurationInMS update")
|
|
628
|
+
|
|
629
|
+
// Test D4: Locked track rejects updates to correctedAt
|
|
630
|
+
log("D4: Updating correctedAt on locked track (should fail)...")
|
|
631
|
+
await assert_throws(
|
|
632
|
+
() => sdk.api.time_tracks.updateOne(locked.id, {
|
|
633
|
+
correctedAt: new Date(),
|
|
634
|
+
originalTotalDurationInMS: 3600000,
|
|
635
|
+
totalDurationInMS: 1800000,
|
|
636
|
+
lockedAt: new Date(),
|
|
637
|
+
lockedByUserId: sdk.userInfo.id,
|
|
638
|
+
} as any),
|
|
639
|
+
"D4: correctedAt update on locked track"
|
|
640
|
+
)
|
|
641
|
+
log("D4: Correctly rejected correction on locked track")
|
|
642
|
+
|
|
643
|
+
// Test D5: Locked track rejects updates to title
|
|
644
|
+
log("D5: Updating title on locked track (should fail)...")
|
|
645
|
+
await assert_throws(
|
|
646
|
+
() => sdk.api.time_tracks.updateOne(locked.id, {
|
|
647
|
+
title: "Should Not Change",
|
|
648
|
+
} as any),
|
|
649
|
+
"D5: title update on locked track"
|
|
650
|
+
)
|
|
651
|
+
log("D5: Correctly rejected title update")
|
|
652
|
+
|
|
653
|
+
// Test D6: Locked track allows updates to review fields
|
|
654
|
+
log("D6: Updating review fields on locked track (should succeed)...")
|
|
655
|
+
const reviewed = await sdk.api.time_tracks.updateOne(locked.id, {
|
|
656
|
+
reviewedAt: new Date(),
|
|
657
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
658
|
+
reviewApproved: true,
|
|
659
|
+
reviewNote: "Looks good",
|
|
660
|
+
} as any)
|
|
661
|
+
assert(reviewed.reviewApproved === true, "reviewApproved should be set")
|
|
662
|
+
assert(reviewed.reviewNote === "Looks good", "reviewNote should be set")
|
|
663
|
+
assert(reviewed.reviewedByUserId === sdkNonAdmin.userInfo.id, "reviewedByUserId should match")
|
|
664
|
+
log("D6: Review fields correctly updatable on locked track")
|
|
665
|
+
|
|
666
|
+
log("All lock enforcement tests passed!")
|
|
667
|
+
|
|
668
|
+
} finally {
|
|
669
|
+
for (const id of trackIds) {
|
|
670
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ============================================================
|
|
676
|
+
// Group E: Edge Cases
|
|
677
|
+
// ============================================================
|
|
678
|
+
export const time_tracks_edge_case_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
679
|
+
log_header("Time Tracks - Edge Case Tests")
|
|
680
|
+
|
|
681
|
+
const trackIds: string[] = []
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
// Test E1: Normal real-time track - lockedAt stays undefined, all fields updatable
|
|
685
|
+
log("E1: Normal real-time track is fully updatable...")
|
|
686
|
+
const track = await sdk.api.time_tracks.createOne({
|
|
687
|
+
title: "Normal Track",
|
|
688
|
+
} as any)
|
|
689
|
+
trackIds.push(track.id)
|
|
690
|
+
|
|
691
|
+
assert(!track.lockedAt, "lockedAt should be undefined for normal track")
|
|
692
|
+
assert(!track.isHistorical, "isHistorical should be undefined for normal track")
|
|
693
|
+
|
|
694
|
+
// Should be able to update title
|
|
695
|
+
const updated = await sdk.api.time_tracks.updateOne(track.id, {
|
|
696
|
+
title: "Updated Normal Track",
|
|
697
|
+
})
|
|
698
|
+
assert(updated.title === "Updated Normal Track", "Title should be updatable on unlocked track")
|
|
699
|
+
|
|
700
|
+
// Should be able to update timestamps
|
|
701
|
+
const withPause = await sdk.api.time_tracks.updateOne(track.id, {
|
|
702
|
+
timestamps: [
|
|
703
|
+
...(track.timestamps || []),
|
|
704
|
+
{ type: 'pause', timestamp: new Date() },
|
|
705
|
+
]
|
|
706
|
+
}, { replaceObjectFields: true })
|
|
707
|
+
assert(withPause.timestamps?.length === 2, "timestamps should be updatable on unlocked track")
|
|
708
|
+
|
|
709
|
+
log("E1: Normal track fully updatable as expected")
|
|
710
|
+
|
|
711
|
+
// Test E2: Original totalDurationInMS preserved after correction
|
|
712
|
+
log("E2: Original duration preserved after correction...")
|
|
713
|
+
const track2 = await sdk.api.time_tracks.createOne({
|
|
714
|
+
title: "Track for Duration Preservation",
|
|
715
|
+
} as any)
|
|
716
|
+
trackIds.push(track2.id)
|
|
717
|
+
|
|
718
|
+
// Close the track
|
|
719
|
+
await sdk.api.time_tracks.updateOne(track2.id, { closedAt: new Date() })
|
|
720
|
+
await wait(undefined, 1500)
|
|
721
|
+
|
|
722
|
+
const closedTrack = await sdk.api.time_tracks.getOne(track2.id)
|
|
723
|
+
const originalDuration = closedTrack.totalDurationInMS!
|
|
724
|
+
|
|
725
|
+
// Apply correction
|
|
726
|
+
const corrected = await sdk.api.time_tracks.updateOne(track2.id, {
|
|
727
|
+
correctedAt: new Date(),
|
|
728
|
+
correctedByUserId: sdk.userInfo.id,
|
|
729
|
+
originalTotalDurationInMS: originalDuration,
|
|
730
|
+
totalDurationInMS: 5000,
|
|
731
|
+
lockedAt: new Date(),
|
|
732
|
+
lockedByUserId: sdk.userInfo.id,
|
|
733
|
+
} as any)
|
|
734
|
+
|
|
735
|
+
assert(corrected.originalTotalDurationInMS === originalDuration,
|
|
736
|
+
`originalTotalDurationInMS should be ${originalDuration}, got ${corrected.originalTotalDurationInMS}`)
|
|
737
|
+
assert(corrected.totalDurationInMS === 5000,
|
|
738
|
+
`totalDurationInMS should be corrected to 5000, got ${corrected.totalDurationInMS}`)
|
|
739
|
+
log("E2: Original duration correctly preserved in originalTotalDurationInMS")
|
|
740
|
+
|
|
741
|
+
log("All edge case tests passed!")
|
|
742
|
+
|
|
743
|
+
} finally {
|
|
744
|
+
for (const id of trackIds) {
|
|
745
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
229
750
|
// Allow running this test file independently
|
|
230
751
|
if (require.main === module) {
|
|
231
752
|
const sdk = new Session({ host })
|
|
@@ -238,15 +759,20 @@ if (require.main === module) {
|
|
|
238
759
|
// Then run API tests
|
|
239
760
|
await setup_tests(sdk, sdkNonAdmin)
|
|
240
761
|
await time_tracks_tests({ sdk, sdkNonAdmin })
|
|
762
|
+
await time_tracks_historical_tests({ sdk, sdkNonAdmin })
|
|
763
|
+
await time_tracks_correction_tests({ sdk, sdkNonAdmin })
|
|
764
|
+
await time_tracks_review_tests({ sdk, sdkNonAdmin })
|
|
765
|
+
await time_tracks_lock_tests({ sdk, sdkNonAdmin })
|
|
766
|
+
await time_tracks_edge_case_tests({ sdk, sdkNonAdmin })
|
|
241
767
|
}
|
|
242
768
|
|
|
243
769
|
runTests()
|
|
244
770
|
.then(() => {
|
|
245
|
-
console.log("
|
|
771
|
+
console.log("Time tracks test suite completed successfully")
|
|
246
772
|
process.exit(0)
|
|
247
773
|
})
|
|
248
774
|
.catch((error) => {
|
|
249
|
-
console.error("
|
|
775
|
+
console.error("Time tracks test suite failed:", error)
|
|
250
776
|
process.exit(1)
|
|
251
777
|
})
|
|
252
778
|
}
|