google-drive-mock 1.0.13 → 1.1.1

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.
@@ -0,0 +1,920 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import {
3
+ getTestConfig,
4
+ TestConfig
5
+ } from './config';
6
+ import { DriveFile } from '../src/store';
7
+
8
+ const randomString = () => Math.random().toString(36).substring(7);
9
+
10
+ const createFileWithContent = async (name: string, content: string, config: TestConfig) => {
11
+ const res = await fetch(`${config.baseUrl}/upload/drive/v3/files?uploadType=media`, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Authorization': `Bearer ${config.token}`,
15
+ 'Content-Type': 'text/plain'
16
+ },
17
+ body: content
18
+ });
19
+ const file = await res.json();
20
+ // V3 standard upload might not set name in body for media upload if not multipart.
21
+ // But store.createFile handles it.
22
+ // To be safe and ensure name is set as expected for query (though create with media upload sets name to Untitled usually),
23
+ // let's update it or use multipart.
24
+ // actually, let's just use the patch to set name/metadata to ensure it's correct for the test.
25
+
26
+ // Better: use multipart or just update after create.
27
+ await fetch(`${config.baseUrl}/drive/v3/files/${file.id}`, {
28
+ method: 'PATCH',
29
+ headers: {
30
+ 'Authorization': `Bearer ${config.token}`,
31
+ 'Content-Type': 'application/json'
32
+ },
33
+ body: JSON.stringify({ name })
34
+ });
35
+
36
+ // Fetch again to get full fields including modifiedTime
37
+ const getRes = await fetch(`${config.baseUrl}/drive/v3/files/${file.id}?fields=*`, {
38
+ headers: { 'Authorization': `Bearer ${config.token}` }
39
+ });
40
+ return await getRes.json();
41
+ };
42
+
43
+ describe('Iterate Changes Queries', () => {
44
+ let config: TestConfig;
45
+ let headers: Record<string, string>;
46
+
47
+ beforeAll(async () => {
48
+ config = await getTestConfig();
49
+ headers = {
50
+ Authorization: `Bearer ${config.token}`
51
+ };
52
+ });
53
+
54
+ it('should find files where last write time was greater than X, sorted by modifiedTime and id, with limit', async () => {
55
+ // Create 3 files with slight delays to ensure different modifiedTimes
56
+ const file1 = await createFileWithContent('file1', randomString(), config);
57
+ await new Promise(r => setTimeout(r, 1100)); // Ensure > 1s diff for reliable sorting if seconds resolution
58
+ const file2 = await createFileWithContent('file2', randomString(), config);
59
+ await new Promise(r => setTimeout(r, 1100));
60
+ const file3 = await createFileWithContent('file3', randomString(), config);
61
+
62
+ // Use file1's modifiedTime as the baseline (X)
63
+ const timeX = file1.modifiedTime;
64
+
65
+ // Query: modifiedTime > X, orderBy modifiedTime asc, name asc (using name as proxy for ID stability in test if needed, but user asked for ID)
66
+ // User asked for: Sorted by write data and id. with limit
67
+ const q = `modifiedTime > '${timeX}' and trashed = false`;
68
+ const orderBy = 'modifiedTime asc, name asc';
69
+ const pageSize = 1;
70
+
71
+ // First page
72
+ const url1 = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=${pageSize}`;
73
+ const res1 = await fetch(url1, { headers });
74
+ if (res1.status !== 200) {
75
+ const txt = await res1.text();
76
+ console.error('Error 1:', txt);
77
+ }
78
+ expect(res1.status).toBe(200);
79
+ const data1 = await res1.json();
80
+
81
+ expect(data1.files.length).toBe(1);
82
+ expect(data1.files[0].id).toBe(file2.id);
83
+
84
+ // If we want to simulate iteration, we would use nextPageToken or just offset logic if we supported it,
85
+ // but here we just test that the query works and LIMIT works.
86
+
87
+ // Verify we can get the next one if we increase limit
88
+ const url2 = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=2`;
89
+ const res2 = await fetch(url2, { headers });
90
+ const data2 = await res2.json();
91
+ expect(data2.files.length).toBe(2);
92
+ expect(data2.files[0].id).toBe(file2.id);
93
+ expect(data2.files[1].id).toBe(file3.id);
94
+ }, 60000);
95
+
96
+ it('should find all files where write time was equal to X, sorted by name, with limit', async () => {
97
+ // Create 3 files effectively at the "same" time.
98
+ // To do this reliably on Real API, we create one, get its time, and then PATCH the others to have that same time (if possible).
99
+ // However, Drive API might not allow arbitrary modifiedTime patching easily without setModifiedDate=true param or similar.
100
+ // Actually, V3 supports modifying modifiedTime.
101
+
102
+ const file1 = await createFileWithContent('file_B_middle', randomString(), config);
103
+ // Get the time from file1 to use as target
104
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}?fields=modifiedTime`, { headers });
105
+ const timeX = (await timeXRes.json()).modifiedTime;
106
+
107
+ // Create two more files
108
+ const file2 = await createFileWithContent('file_A_first', randomString(), config);
109
+ const file3 = await createFileWithContent('file_C_last', randomString(), config);
110
+
111
+ // Patch file2 and file3 to have the SAME modifiedTime as file1
112
+ // We need to wait a bit to ensure they would naturally have different times if we didn't patch,
113
+ // to prove the patch worked and we are sorting by name not time.
114
+ await new Promise(r => setTimeout(r, 1100));
115
+
116
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
117
+ await fetch(`${config.baseUrl}/drive/v3/files/${file2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
118
+ await fetch(`${config.baseUrl}/drive/v3/files/${file3.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
119
+
120
+ const q = `modifiedTime = '${timeX}' and trashed = false`;
121
+ const orderBy = 'name asc';
122
+ const pageSize = 10;
123
+
124
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=${pageSize}&fields=files(id,name,modifiedTime)`;
125
+ const res = await fetch(url, { headers });
126
+ expect(res.status).toBe(200);
127
+ const data = await res.json();
128
+
129
+ // Should find all 3 files
130
+ const relevantFiles = data.files.filter((f: DriveFile) => [file1.id, file2.id, file3.id].includes(f.id));
131
+ expect(relevantFiles.length).toBe(3);
132
+
133
+ // Verify they are sorted by name: A, B, C
134
+ expect(relevantFiles[0].name).toBe('file_A_first');
135
+ expect(relevantFiles[1].name).toBe('file_B_middle');
136
+ expect(relevantFiles[2].name).toBe('file_C_last');
137
+
138
+ // Verify times
139
+ relevantFiles.forEach((f: DriveFile) => {
140
+ if (!f.modifiedTime) {
141
+ console.error('Missing modifiedTime for file:', f.id, f.name);
142
+ }
143
+ expect(new Date(f.modifiedTime).toISOString()).toBe(new Date(timeX).toISOString());
144
+ });
145
+ }, 60000);
146
+
147
+ it('should find files where write time = X AND inside a specific parent folder, sorted by name', async () => {
148
+ // 1. Create a parent folder
149
+ const parentRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
150
+ method: 'POST',
151
+ headers: { ...headers, 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({
153
+ name: 'ParentFolder_EqualTime_' + randomString(),
154
+ mimeType: 'application/vnd.google-apps.folder'
155
+ })
156
+ });
157
+ expect(parentRes.status).toBe(200);
158
+ const parentId = (await parentRes.json()).id;
159
+
160
+ // 2. Create 3 files IN parent + 1 file OUTSIDE parent
161
+ // We want them all to have the SAME modifiedTime eventually.
162
+
163
+ // Create baseline file in parent
164
+ const file1 = await createFileWithContent('file_B_middle', randomString(), config);
165
+ // Move to parent
166
+ await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}?addParents=${parentId}`, { method: 'PATCH', headers });
167
+
168
+ // Get target time
169
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}?fields=modifiedTime`, { headers });
170
+ const timeX = (await timeXRes.json()).modifiedTime;
171
+
172
+ // Create other files
173
+ const file2 = await createFileWithContent('file_A_first', randomString(), config);
174
+ await fetch(`${config.baseUrl}/drive/v3/files/${file2.id}?addParents=${parentId}`, { method: 'PATCH', headers });
175
+
176
+ const file3 = await createFileWithContent('file_C_last', randomString(), config);
177
+ await fetch(`${config.baseUrl}/drive/v3/files/${file3.id}?addParents=${parentId}`, { method: 'PATCH', headers });
178
+
179
+ const fileOutside = await createFileWithContent('file_Outside', randomString(), config);
180
+
181
+ // DELAY to ensure natural time diff, then PATCH all to timeX
182
+ await new Promise(r => setTimeout(r, 1100));
183
+
184
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
185
+ await fetch(`${config.baseUrl}/drive/v3/files/${file2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
186
+ await fetch(`${config.baseUrl}/drive/v3/files/${file3.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
187
+ await fetch(`${config.baseUrl}/drive/v3/files/${fileOutside.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
188
+
189
+ // 3. Query: modifiedTime = X AND parentId in parents
190
+ const q = `modifiedTime = '${timeX}' and '${parentId}' in parents and trashed = false`;
191
+ const orderBy = 'name asc';
192
+
193
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,modifiedTime,parents)`;
194
+ const res = await fetch(url, { headers });
195
+ expect(res.status).toBe(200);
196
+ const data = await res.json();
197
+
198
+ // 4. Verify results
199
+ // Should find file1, file2, file3
200
+ // Should NOT find fileOutside
201
+ const ids = data.files.map((f: DriveFile) => f.id);
202
+ expect(ids).toContain(file1.id);
203
+ expect(ids).toContain(file2.id);
204
+ expect(ids).toContain(file3.id);
205
+ expect(ids).not.toContain(fileOutside.id);
206
+ expect(data.files.length).toBe(3);
207
+
208
+ // Verify Sort Order
209
+ expect(data.files[0].name).toBe('file_A_first');
210
+ expect(data.files[1].name).toBe('file_B_middle');
211
+ expect(data.files[2].name).toBe('file_C_last');
212
+
213
+ }, 60000);
214
+
215
+ it('should find files where write time >= X, sorted by modifiedTime and name', async () => {
216
+ // 1. Create file OLD (should be excluded)
217
+ const fileOld = await createFileWithContent('file_A_Old', randomString(), config);
218
+
219
+ // Wait to ensure distinct time
220
+ await new Promise(r => setTimeout(r, 1500));
221
+
222
+ // 2. Create ISO-time target files (Equal Time)
223
+ // We create one, get its time, then create another and patch it to match.
224
+ const fileMiddle1 = await createFileWithContent('file_B_Middle1', randomString(), config);
225
+ // Get target time
226
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
227
+ const timeX = (await timeXRes.json()).modifiedTime;
228
+
229
+ // Create second middle file
230
+ const fileMiddle2 = await createFileWithContent('file_B_Middle2', randomString(), config);
231
+
232
+ // Patch fileMiddle2 to match fileMiddle1 time
233
+ await new Promise(r => setTimeout(r, 1100)); // Wait before patching to ensure it would be different otherwise
234
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
235
+ await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
236
+
237
+ // Wait again for New file
238
+ await new Promise(r => setTimeout(r, 1500));
239
+
240
+ // 3. Create file New (should be included, greater match logic)
241
+ const fileNew = await createFileWithContent('file_C_New', randomString(), config);
242
+
243
+ // 4. Query: modifiedTime >= timeX
244
+ // Sort by modifiedTime asc, name asc
245
+ const q = `modifiedTime >= '${timeX}' and trashed = false`;
246
+ const orderBy = 'modifiedTime asc, name asc';
247
+
248
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,modifiedTime)`;
249
+ const res = await fetch(url, { headers });
250
+ if (res.status !== 200) {
251
+ console.error('Error response:', await res.text());
252
+ }
253
+ expect(res.status).toBe(200);
254
+ const data = await res.json();
255
+
256
+ // 5. Verify results
257
+ const resultIds = data.files.map((f: DriveFile) => f.id);
258
+
259
+ // Should NOT contain Old
260
+ expect(resultIds).not.toContain(fileOld.id);
261
+
262
+ // Should contain Middle1, Middle2, New
263
+ const relevantFiles = data.files.filter((f: DriveFile) => [fileMiddle1.id, fileMiddle2.id, fileNew.id].includes(f.id));
264
+ expect(relevantFiles.length).toBe(3);
265
+
266
+ // Verify Order
267
+ // Expect: [Middle1/Middle2 (sorted by NAME), New]
268
+
269
+ // First two should be the middle ones
270
+ const firstTwoIds = [relevantFiles[0].id, relevantFiles[1].id];
271
+ expect(firstTwoIds).toContain(fileMiddle1.id);
272
+ expect(firstTwoIds).toContain(fileMiddle2.id);
273
+
274
+ // Verify secondary sort of first two by NAME
275
+ const expectedFirst = fileMiddle1.name < fileMiddle2.name ? fileMiddle1.id : fileMiddle2.id;
276
+ const expectedSecond = fileMiddle1.name < fileMiddle2.name ? fileMiddle2.id : fileMiddle1.id;
277
+ expect(relevantFiles[0].id).toBe(expectedFirst);
278
+ expect(relevantFiles[1].id).toBe(expectedSecond);
279
+
280
+ // Third should be New
281
+ expect(relevantFiles[2].id).toBe(fileNew.id);
282
+
283
+ // Verify timestamps
284
+ expect(new Date(relevantFiles[0].modifiedTime).toISOString()).toBe(new Date(timeX).toISOString());
285
+ expect(new Date(relevantFiles[1].modifiedTime).toISOString()).toBe(new Date(timeX).toISOString());
286
+ expect(new Date(relevantFiles[2].modifiedTime) > new Date(timeX)).toBe(true);
287
+
288
+ }, 60000);
289
+
290
+ // Test for Name >= Query support
291
+ // VERIFIED: UNSUPPORTED. Returns 500 Internal Error from Google Drive API.
292
+ /*
293
+ it('should find files where name >= X', async () => {
294
+ const fileA = await createFileWithContent('file_A_NameCheck', randomString(), config);
295
+ const fileB = await createFileWithContent('file_B_NameCheck', randomString(), config);
296
+ const fileC = await createFileWithContent('file_C_NameCheck', randomString(), config);
297
+
298
+ // Query: name >= 'file_B_NameCheck'
299
+ // Expect to find B and C, but not A.
300
+ const q = `name >= 'file_B_NameCheck' and trashed = false`;
301
+ const orderBy = 'name asc';
302
+
303
+ console.log('Query:', q);
304
+
305
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
306
+ const res = await fetch(url, { headers });
307
+
308
+ if (res.status !== 200) {
309
+ console.error('Error response:', await res.text());
310
+ }
311
+ // We expect 200 if supported, or 400/500 if not.
312
+ // We will assert 200 to see if it passes.
313
+ expect(res.status).toBe(200);
314
+
315
+ const data = await res.json();
316
+ const names = data.files.map((f: DriveFile) => f.name);
317
+
318
+ expect(names).toContain('file_B_NameCheck');
319
+ expect(names).toContain('file_C_NameCheck');
320
+ expect(names).not.toContain('file_A_NameCheck');
321
+ }, 60000);
322
+ /*
323
+ it('should find files where name >= X', async () => {
324
+ // ... (existing commented code)
325
+ }, 60000);
326
+ */
327
+
328
+ // Test for ID >= Query support
329
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request from Google Drive API.
330
+ /*
331
+ it('should find files where id >= X', async () => {
332
+ const fileA = await createFileWithContent('file_A_IdCheck', randomString(), config);
333
+ const fileB = await createFileWithContent('file_B_IdCheck', randomString(), config);
334
+ const fileC = await createFileWithContent('file_C_IdCheck', randomString(), config);
335
+
336
+ // Sort IDs to know order
337
+ const ids = [fileA.id, fileB.id, fileC.id].sort();
338
+ const pivotId = ids[1]; // Middle ID
339
+
340
+ // Query: id >= pivotId
341
+ // Expect to find pivotId and larger ID.
342
+ const q = `id >= '${pivotId}' and trashed = false`;
343
+ const orderBy = 'createdTime asc'; // Sort logic for ID is unsupported, use createdTime or name
344
+
345
+ console.log('Query:', q);
346
+
347
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
348
+ const res = await fetch(url, { headers });
349
+
350
+ if (res.status !== 200) {
351
+ console.error('Error response:', await res.text());
352
+ }
353
+ // We expect 200 if supported, or 400/500 if not.
354
+ expect(res.status).toBe(200);
355
+
356
+ const data = await res.json();
357
+ const resultIds = data.files.map((f: DriveFile) => f.id);
358
+
359
+ expect(resultIds).toContain(ids[1]);
360
+ expect(resultIds).toContain(ids[2]);
361
+ expect(resultIds).not.toContain(ids[0]);
362
+ }, 60000);
363
+ */
364
+
365
+ // MongoDB-style keyset pagination query using 'name'
366
+ // VERIFIED: This query is NOT supported by Google Drive API.
367
+ // 'name > ...' returns 500 Internal Error on Real API.
368
+ // 'id > ...' returns 400 Bad Request on Real API.
369
+ // it('should find files using keyset pagination: (modifiedTime > T) OR (modifiedTime = T AND name > N)', async () => {
370
+ // // 1. Create file OLD (should be excluded)
371
+ // const fileOld = await createFileWithContent('file_A_Old', randomString(), config);
372
+
373
+ // // Wait
374
+ // await new Promise(r => setTimeout(r, 1500));
375
+
376
+ // // 2. Create ISO-time target files (Equal Time)
377
+ // // We need explicit names to test > logic.
378
+ // const name1 = 'file_B_Middle1';
379
+ // const name2 = 'file_B_Middle2';
380
+
381
+ // const fileMiddle1 = await createFileWithContent(name1, randomString(), config);
382
+ // // Get target time
383
+ // const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
384
+ // const timeX = (await timeXRes.json()).modifiedTime;
385
+
386
+ // // Create second middle file
387
+ // const fileMiddle2 = await createFileWithContent(name2, randomString(), config);
388
+
389
+ // // Patch fileMiddle2 to match fileMiddle1 time
390
+ // await new Promise(r => setTimeout(r, 1100));
391
+ // const patchBody = JSON.stringify({ modifiedTime: timeX });
392
+ // await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
393
+
394
+ // // Wait again for New file
395
+ // await new Promise(r => setTimeout(r, 1500));
396
+
397
+ // // 3. Create file New (should be included, greater match logic)
398
+ // const fileNew = await createFileWithContent('file_C_New', randomString(), config);
399
+
400
+ // // Scenario: We want to page after 'file_B_Middle1'.
401
+ // // So we want (modifiedTime > timeX) OR (modifiedTime = timeX AND name > 'file_B_Middle1').
402
+ // // Since names are sorted Middle1 < Middle2 < C_New (alphabetical),
403
+ // // we expect to find Middle2 and C_New.
404
+
405
+ // const cursorName = name1;
406
+ // const cursorTime = timeX;
407
+
408
+
409
+ // // Simplified query to test 'name >' operator support first
410
+ // const q = `name > '${cursorName}' and trashed = false`;
411
+ // const orderBy = 'name asc';
412
+
413
+ // console.log('Query:', q);
414
+
415
+ // console.log('Query:', q);
416
+
417
+ // const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,modifiedTime)`;
418
+ // const res = await fetch(url, { headers });
419
+
420
+ // if (res.status !== 200) {
421
+ // console.error('Error response:', await res.text());
422
+ // }
423
+ // expect(res.status).toBe(200);
424
+ // const data = await res.json();
425
+
426
+ // const resultIds = data.files.map((f: DriveFile) => f.id);
427
+
428
+ // // Must contain Middle2 and fileNew
429
+ // expect(resultIds).toContain(fileMiddle2.id);
430
+ // expect(resultIds).toContain(fileNew.id);
431
+
432
+ // // Must NOT contain Middle1 (it equals cursor, we want >)
433
+ // expect(resultIds).not.toContain(fileMiddle1.id);
434
+ // expect(resultIds).not.toContain(fileOld.id);
435
+
436
+ // }, 60000);
437
+
438
+ it('should iterate via changes tokens with specific fields', async () => {
439
+ // User request verification:
440
+ // const params = new URLSearchParams({
441
+ // pageToken: checkpoint.pageToken, // we need to get a start page token first
442
+ // pageSize: String(batchSize),
443
+ // includeItemsFromAllDrives: "true",
444
+ // supportsAllDrives: "true",
445
+ // includeRemoved: "true",
446
+ // fields: "changes(fileId,removed,file(id,name,parents,trashed)),nextPageToken,newStartPageToken",
447
+ // });
448
+
449
+ // 1. Get Start Page Token
450
+ const startTokenUrl = `${config.baseUrl}/drive/v3/changes/startPageToken?supportsAllDrives=true`;
451
+ const startTokenRes = await fetch(startTokenUrl, { headers });
452
+ expect(startTokenRes.status).toBe(200);
453
+ const startTokenData = await startTokenRes.json();
454
+ const startPageToken = startTokenData.startPageToken;
455
+ expect(startPageToken).toBeDefined();
456
+
457
+ // 2. Make some changes
458
+ const file1 = await createFileWithContent('change-file-1', randomString(), config);
459
+ await new Promise(r => setTimeout(r, 1000));
460
+
461
+ // Trash a file to test includeRemoved/removed field
462
+ const trashRes = await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}`, {
463
+ method: 'PATCH',
464
+ headers: { ...headers, 'Content-Type': 'application/json' },
465
+ body: JSON.stringify({ trashed: true })
466
+ });
467
+ expect(trashRes.status).toBe(200);
468
+
469
+ // 3. List Changes
470
+ const params = new URLSearchParams({
471
+ pageToken: startPageToken,
472
+ pageSize: "10",
473
+ includeItemsFromAllDrives: "true",
474
+ supportsAllDrives: "true",
475
+ includeRemoved: "true",
476
+ fields: "changes(fileId,removed,file(id,name,parents,trashed)),nextPageToken,newStartPageToken"
477
+ });
478
+
479
+ const listUrl = `${config.baseUrl}/drive/v3/changes?${params.toString()}`;
480
+ const listRes = await fetch(listUrl, { headers });
481
+
482
+ if (listRes.status !== 200) {
483
+ console.error('Changes List Error:', await listRes.text());
484
+ }
485
+ expect(listRes.status).toBe(200);
486
+ const data = await listRes.json();
487
+
488
+ expect(data.changes).toBeDefined();
489
+ // We expect at least the creation and trash of file1.
490
+ // Note: Real API might batch them or show multiple entries.
491
+ // We just verify structure and presence of fields requested.
492
+
493
+ if (data.changes.length > 0) {
494
+ const change = data.changes[0];
495
+ expect(change.fileId).toBeDefined();
496
+ // removed can be boolean
497
+ expect(change.removed).toBeDefined();
498
+ if (!change.removed && change.file) {
499
+ const file = change.file as DriveFile;
500
+ expect(file.id).toBeDefined();
501
+ expect(file.name).toBeDefined();
502
+ }
503
+ }
504
+ }, 60000);
505
+
506
+ it('should find files where write time > X AND inside a specific parent folder', async () => {
507
+ // 1. Create a parent folder
508
+ const parentRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
509
+ method: 'POST',
510
+ headers: { ...headers, 'Content-Type': 'application/json' },
511
+ body: JSON.stringify({
512
+ name: 'ParentFolder_' + randomString(),
513
+ mimeType: 'application/vnd.google-apps.folder'
514
+ })
515
+ });
516
+ expect(parentRes.status).toBe(200);
517
+ const parent = await parentRes.json();
518
+ const parentId = parent.id;
519
+
520
+ // 2. Create 3 files:
521
+ // - One IN parent, modified OLD
522
+ // - One IN parent, modified NEW
523
+ // - One OUTSIDE parent, modified NEW
524
+
525
+ // Define "Old" and "New" times.
526
+ // We'll just create them sequentially with delays.
527
+
528
+ // File 1: In parent, first.
529
+ const file1 = await createFileWithContent('FileInParentOld', 'content1', config);
530
+ // Move to parent
531
+ const moveRes1 = await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}?addParents=${parentId}`, {
532
+ method: 'PATCH', headers
533
+ });
534
+ expect(moveRes1.status).toBe(200);
535
+
536
+ await new Promise(r => setTimeout(r, 1100));
537
+
538
+ // This time will be our X
539
+ // We want to find files modified > this time.
540
+ // So file1 should NOT be found (it is <= itself/X, or we rely on creating a checkpoint after it).
541
+ // Let's create a checkpoint file or just use file1's time.
542
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${file1.id}?fields=modifiedTime`, { headers });
543
+ const timeXJson = await timeXRes.json();
544
+ const timeX = timeXJson.modifiedTime;
545
+
546
+ await new Promise(r => setTimeout(r, 1100));
547
+
548
+ // File 2: In parent, NEW (should be found)
549
+ const file2 = await createFileWithContent('FileInParentNew', 'content2', config);
550
+ const moveRes2 = await fetch(`${config.baseUrl}/drive/v3/files/${file2.id}?addParents=${parentId}`, {
551
+ method: 'PATCH', headers
552
+ });
553
+ expect(moveRes2.status).toBe(200);
554
+
555
+ // File 3: Outside parent, NEW (should NOT be found)
556
+ await createFileWithContent('FileOutsideNew', 'content3', config);
557
+ // (Default parent or root, explicitly not our parentId)
558
+
559
+ // 3. Query: modifiedTime > X AND 'parentId' in parents
560
+ const q = `modifiedTime > '${timeX}' and '${parentId}' in parents and trashed = false`;
561
+ const orderBy = 'modifiedTime asc, name asc';
562
+
563
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,parents,modifiedTime)`;
564
+ const res = await fetch(url, { headers });
565
+ expect(res.status).toBe(200);
566
+ const data = await res.json();
567
+
568
+ const matchingFiles = data.files.filter((f: DriveFile) => f.id === file2.id);
569
+ const nonMatchingFile1 = data.files.filter((f: DriveFile) => f.id === file1.id);
570
+
571
+ // Should find file2
572
+ expect(matchingFiles.length).toBe(1);
573
+ expect(matchingFiles[0].id).toBe(file2.id);
574
+
575
+ // Should NOT find file1 (too old)
576
+ expect(nonMatchingFile1.length).toBe(0);
577
+
578
+ // Should NOT find file3 (wrong parent) check manually in case it returned it
579
+ const file3Found = data.files.find((f: DriveFile) => f.name === 'FileOutsideNew');
580
+ expect(file3Found).toBeUndefined();
581
+
582
+ }, 60000);
583
+
584
+ it('should paginate through files using nextPageToken', async () => {
585
+ // Create files
586
+ const totalFiles = 6;
587
+ const baseName = 'PaginatedFile_' + randomString();
588
+ for (let i = 0; i < totalFiles; i++) {
589
+ await createFileWithContent(`${baseName}_${i}`, `content_${i}`, config);
590
+ // Small delay to ensure order if we sort by time, but we'll sort by name to be deterministic
591
+ }
592
+
593
+ const q = `name contains '${baseName}' and trashed = false`;
594
+ const orderBy = 'name asc';
595
+ const pageSize = 2;
596
+ const collectedFiles: DriveFile[] = [];
597
+ let pageToken: string | undefined;
598
+
599
+ // Iterate pages until no token
600
+ do {
601
+ const url: string = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=${pageSize}` + (pageToken ? `&pageToken=${pageToken}` : '');
602
+ const res = await fetch(url, { headers });
603
+
604
+ if (res.status !== 200) {
605
+ console.error('Pagination Error:', await res.text());
606
+ }
607
+ expect(res.status).toBe(200);
608
+ const data = await res.json();
609
+
610
+ if (data.files) {
611
+ collectedFiles.push(...data.files);
612
+ }
613
+ pageToken = data.nextPageToken;
614
+
615
+ // Safety break to prevent infinite loops if API is broken
616
+ if (collectedFiles.length > totalFiles + 10) break;
617
+ } while (pageToken);
618
+
619
+ // Verify total
620
+ // Note: Drive API matching is eventually consistent.
621
+ // We might need to retry or wait if count is not yet totalFiles,
622
+ // but since we created them with delays, it usually works.
623
+ // If it flakes on count, we might need a retry loop wrapper around the whole test or query.
624
+ expect(collectedFiles.length).toBe(totalFiles);
625
+
626
+ // Verify unique IDs
627
+ const ids = new Set(collectedFiles.map(f => f.id));
628
+ expect(ids.size).toBe(totalFiles);
629
+
630
+ }, 120000); // 25 files creation might take a bit
631
+
632
+ // Test for ID > Query support (v3)
633
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request on v3.
634
+ /*
635
+ it('should find files where id > X on v3 API', async () => {
636
+ const fileA = await createFileWithContent('file_A_IdCheck_v3', randomString(), config);
637
+ const fileB = await createFileWithContent('file_B_IdCheck_v3', randomString(), config);
638
+ const fileC = await createFileWithContent('file_C_IdCheck_v3', randomString(), config);
639
+
640
+ // Sort IDs to know order
641
+ const ids = [fileA.id, fileB.id, fileC.id].sort();
642
+ const pivotId = ids[1]; // Middle ID
643
+
644
+ // Query: id > pivotId
645
+ const q = `id > '${pivotId}' and trashed = false`;
646
+ const orderBy = 'createdTime asc';
647
+
648
+ console.log('Query v3:', q);
649
+
650
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
651
+ const res = await fetch(url, { headers });
652
+
653
+ if (res.status !== 200) {
654
+ console.error('Error response v3:', await res.text());
655
+ }
656
+ expect(res.status).toBe(200);
657
+
658
+ const data = await res.json();
659
+ const resultIds = data.files.map((f: DriveFile) => f.id);
660
+
661
+ expect(resultIds).toContain(ids[2]);
662
+ expect(resultIds).not.toContain(ids[1]); // strict >
663
+ expect(resultIds).not.toContain(ids[0]);
664
+ }, 60000);
665
+ */
666
+
667
+ // Test for ID > Query support on v2 API
668
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request on v2.
669
+ /*
670
+ it('should find files where id > X on v2 API', async () => {
671
+ // ... (existing code)
672
+ }, 60000);
673
+ */
674
+
675
+ // MongoDB-style keyset pagination query using 'title' on v2 API
676
+ // VERIFIED: UNSUPPORTED. Returns 500 Internal Error on v2 for this complex query.
677
+ /*
678
+ it('should find files using keyset pagination on v2: (modifiedDate > T) OR (modifiedDate = T AND title > N)', async () => {
679
+ // 1. Create file OLD (should be excluded)
680
+ const fileOld = await createFileWithContent('file_A_Old_v2', randomString(), config);
681
+
682
+ // Wait
683
+ await new Promise(r => setTimeout(r, 1500));
684
+
685
+ // 2. Create ISO-time target files (Equal Time)
686
+ // We need explicit names to test > logic.
687
+ const name1 = 'file_B_Middle1_v2';
688
+ const name2 = 'file_B_Middle2_v2';
689
+
690
+ const fileMiddle1 = await createFileWithContent(name1, randomString(), config);
691
+ // Get target time (using v3 to get precise time, assuming v2 sees same time)
692
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
693
+ const timeX = (await timeXRes.json()).modifiedTime;
694
+
695
+ // Create second middle file
696
+ const fileMiddle2 = await createFileWithContent(name2, randomString(), config);
697
+
698
+ // Patch fileMiddle2 to match fileMiddle1 time
699
+ await new Promise(r => setTimeout(r, 1100));
700
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
701
+ await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
702
+
703
+ // Wait again for New file
704
+ await new Promise(r => setTimeout(r, 1500));
705
+
706
+ // 3. Create file New (should be included, greater match logic)
707
+ const fileNew = await createFileWithContent('file_C_New_v2', randomString(), config);
708
+
709
+ // Scenario: We want to page after 'file_B_Middle1'.
710
+ // So we want (modifiedDate > timeX) OR (modifiedDate = timeX AND title > 'file_B_Middle1_v2').
711
+
712
+ const cursorTitle = name1;
713
+ const cursorTime = timeX;
714
+
715
+ // Query using v2 fields: modifiedDate and title
716
+ const q = `(modifiedDate > '${cursorTime}') or (modifiedDate = '${cursorTime}' and title > '${cursorTitle}') and trashed = false`;
717
+ // v2 uses 'title' for name
718
+
719
+ console.log('Query v2 Keyset:', q);
720
+
721
+ const url = `${config.baseUrl}/drive/v2/files?q=${encodeURIComponent(q)}`;
722
+ const res = await fetch(url, { headers });
723
+
724
+ if (res.status !== 200) {
725
+ console.error('Error response v2 Keyset:', await res.text());
726
+ }
727
+ expect(res.status).toBe(200);
728
+ const data = await res.json();
729
+
730
+ const resultIds = data.items.map((f: any) => f.id);
731
+
732
+ // Must contain Middle2 and fileNew
733
+ expect(resultIds).toContain(fileMiddle2.id);
734
+ expect(resultIds).toContain(fileNew.id);
735
+
736
+ // Must NOT contain Middle1 (it equals cursor, we want >)
737
+ expect(resultIds).not.toContain(fileMiddle1.id);
738
+ expect(resultIds).not.toContain(fileOld.id);
739
+ }, 60000);
740
+ */
741
+
742
+ // Test for Paginated Changes in Parent Folder (v3)
743
+ it('should paginate through "changes" (files > time) in a specific parent folder (v3)', async () => {
744
+ // 1. Create Parent Folder
745
+ const resP = await fetch(`${config.baseUrl}/drive/v3/files`, {
746
+ method: 'POST',
747
+ headers: { ...headers, 'Content-Type': 'application/json' },
748
+ body: JSON.stringify({
749
+ name: 'ParentFolder_Pagination_v3_' + randomString(),
750
+ mimeType: 'application/vnd.google-apps.folder'
751
+ })
752
+ });
753
+ expect(resP.status).toBe(200);
754
+ const parentId = (await resP.json()).id;
755
+
756
+ // 2. Create files
757
+ const totalFiles = 6;
758
+ const baseName = 'InFolder_v3_' + randomString();
759
+
760
+ // Create files
761
+ for (let i = 0; i < totalFiles; i++) {
762
+ // Use body for parents to be safe
763
+ const res = await fetch(`${config.baseUrl}/drive/v3/files?fields=id,parents`, {
764
+ method: 'POST',
765
+ headers: { ...headers, 'Content-Type': 'application/json' },
766
+ body: JSON.stringify({
767
+ name: `${baseName}_${i}`,
768
+ mimeType: 'text/plain',
769
+ parents: [parentId]
770
+ })
771
+ });
772
+ if (res.status !== 200) {
773
+ console.error('Create file failed:', await res.text());
774
+ }
775
+ expect(res.status).toBe(200);
776
+ const created = await res.json();
777
+ if (!created.parents || !created.parents.includes(parentId)) {
778
+ console.error('File created but parent missing:', created);
779
+ }
780
+ }
781
+
782
+ // Wait for potential eventual consistency/indexing
783
+ console.log('Waiting 5s for indexing...');
784
+ await new Promise(r => setTimeout(r, 5000));
785
+
786
+ // Get one file to check time
787
+ const listOne = await fetch(`${config.baseUrl}/drive/v3/files?q='${parentId}' in parents&pageSize=1&fields=files(modifiedTime)`, { headers });
788
+ const oneData = await listOne.json();
789
+ if (!oneData.files || oneData.files.length === 0) {
790
+ throw new Error('No files found in parent check!');
791
+ }
792
+ const refTime = oneData.files[0].modifiedTime;
793
+ console.log('Reference File Time:', refTime);
794
+
795
+ // Filter: modifiedTime > refTime - 1 hour (to safely include all)
796
+ const startTime = new Date(new Date(refTime).getTime() - 3600000).toISOString();
797
+ console.log('Filter Start Time:', startTime);
798
+
799
+ // 3. Query with Pagination
800
+ const q = `'${parentId}' in parents and modifiedTime > '${startTime}' and trashed = false`;
801
+ const orderBy = 'modifiedTime asc';
802
+ const pageSize = 2; // Force pagination
803
+
804
+ const collectedFiles: DriveFile[] = [];
805
+ let pageToken: string | undefined;
806
+
807
+ console.log('Query v3 Parent Pagination:', q);
808
+
809
+ do {
810
+ const url: string = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=${pageSize}` + (pageToken ? `&pageToken=${pageToken}` : '');
811
+ const res = await fetch(url, { headers });
812
+
813
+ if (res.status !== 200) {
814
+ console.error('Pagination Error v3:', await res.text());
815
+ }
816
+ expect(res.status).toBe(200);
817
+ const data = await res.json();
818
+
819
+ if (data.files) {
820
+ console.log(`Page returned ${data.files.length} files. NextToken: ${!!data.nextPageToken}`);
821
+ collectedFiles.push(...data.files);
822
+ }
823
+ pageToken = data.nextPageToken;
824
+
825
+ if (collectedFiles.length > totalFiles + 5) break;
826
+ } while (pageToken);
827
+
828
+ expect(collectedFiles.length).toBe(totalFiles);
829
+ const names = collectedFiles.map(f => f.name);
830
+ expect(names).toHaveLength(totalFiles);
831
+ }, 120000);
832
+
833
+ // Test for Paginated Changes in Parent Folder (v2)
834
+ it('should paginate through "changes" (files > time) in a specific parent folder (v2)', async () => {
835
+ // 1. Create Parent Folder (v3 create is fine, reusable for v2 test)
836
+ const resP = await fetch(`${config.baseUrl}/drive/v3/files`, {
837
+ method: 'POST',
838
+ headers: { ...headers, 'Content-Type': 'application/json' },
839
+ body: JSON.stringify({
840
+ name: 'ParentFolder_Pagination_v2_' + randomString(),
841
+ mimeType: 'application/vnd.google-apps.folder'
842
+ })
843
+ });
844
+ expect(resP.status).toBe(200);
845
+ const parentId = (await resP.json()).id;
846
+
847
+ // 2. Create files (v3 create is fine)
848
+ const totalFiles = 6;
849
+ const baseName = 'InFolder_v2_' + randomString();
850
+
851
+ for (let i = 0; i < totalFiles; i++) {
852
+ const res = await fetch(`${config.baseUrl}/drive/v3/files?fields=id,parents`, {
853
+ method: 'POST',
854
+ headers: { ...headers, 'Content-Type': 'application/json' },
855
+ body: JSON.stringify({
856
+ name: `${baseName}_${i}`,
857
+ mimeType: 'text/plain',
858
+ parents: [parentId]
859
+ })
860
+ });
861
+ expect(res.status).toBe(200);
862
+ }
863
+
864
+ console.log('Waiting 5s for indexing (v2)...');
865
+ await new Promise(r => setTimeout(r, 5000));
866
+
867
+ // Get one file to check time (v2 uses modifiedDate)
868
+ // v2 list: items
869
+ const listOne = await fetch(`${config.baseUrl}/drive/v2/files?q='${parentId}' in parents&maxResults=1`, { headers });
870
+ if (listOne.status !== 200) {
871
+ console.error('v2 check failed:', await listOne.text());
872
+ }
873
+ const oneData = await listOne.json();
874
+
875
+ if (!oneData.items || oneData.items.length === 0) {
876
+ throw new Error('No files found in parent check v2!');
877
+ }
878
+ const refTime = oneData.items[0].modifiedDate;
879
+ console.log('Reference File Time v2:', refTime);
880
+
881
+ // Filter: modifiedDate > refTime - 1 hour
882
+ const startTime = new Date(new Date(refTime).getTime() - 3600000).toISOString();
883
+ console.log('Filter Start Time v2:', startTime);
884
+
885
+ // 3. Query with Pagination (v2 syntax)
886
+ const q = `'${parentId}' in parents and modifiedDate > '${startTime}' and trashed = false`;
887
+ const orderBy = 'modifiedDate asc';
888
+ const pageSize = 2; // maxResults
889
+
890
+ // v2 uses 'items' and 'title', not 'name'
891
+ interface DriveV2File { id: string; title: string; modifiedDate: string; }
892
+ const collectedFiles: DriveV2File[] = [];
893
+ let pageToken: string | undefined;
894
+
895
+ console.log('Query v2 Parent Pagination:', q);
896
+
897
+ do {
898
+ const url: string = `${config.baseUrl}/drive/v2/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&maxResults=${pageSize}` + (pageToken ? `&pageToken=${pageToken}` : '');
899
+ const res = await fetch(url, { headers });
900
+
901
+ if (res.status !== 200) {
902
+ console.error('Pagination Error v2:', await res.text());
903
+ }
904
+ expect(res.status).toBe(200);
905
+ const data = await res.json();
906
+
907
+ if (data.items) {
908
+ console.log(`Page returned ${data.items.length} items. NextToken: ${!!data.nextPageToken}`);
909
+ collectedFiles.push(...data.items);
910
+ }
911
+ pageToken = data.nextPageToken;
912
+
913
+ if (collectedFiles.length > totalFiles + 5) break;
914
+ } while (pageToken);
915
+
916
+ expect(collectedFiles.length).toBe(totalFiles);
917
+ const names = collectedFiles.map((f) => f.title); // v2 uses title
918
+ expect(names).toHaveLength(totalFiles);
919
+ }, 120000);
920
+ });