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.
- package/.agent/rules/project-guidelines.md +5 -0
- package/.github/workflows/release.yml +12 -4
- package/dist/batch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mappers.d.ts +8 -0
- package/dist/mappers.js +79 -0
- package/dist/routes/v3.js +68 -6
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/src/batch.ts +2 -1
- package/src/index.ts +1 -1
- package/src/mappers.ts +84 -0
- package/src/routes/v3.ts +71 -5
- package/src/store.ts +1 -1
- package/test/batch_insert_download.test.ts +150 -0
- package/test/concurrent_fetch.test.ts +17 -10
- package/test/dates_and_sorting.test.ts +0 -2
- package/test/folder_query.test.ts +177 -0
- package/test/iterate_changes.test.ts +920 -0
- package/test/parallel_update.test.ts +138 -0
- package/test/url_parameters.test.ts +76 -0
|
@@ -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
|
+
});
|